diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50b8900e..f9b6b2ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,12 +54,12 @@ jobs: - name: Build CLI (release) run: cargo build -p weaveffi-cli --release - - name: Generate bindings from calculator IDL + - name: Generate bindings from calculator source shell: bash run: | BIN=target/release/weaveffi [ "${{ runner.os }}" = "Windows" ] && BIN="${BIN}.exe" - "$BIN" generate samples/calculator/calculator.yml -o generated + "$BIN" generate samples/calculator/src/lib.rs -o generated - name: Build sample producer cdylibs (debug) run: cargo build -p calculator -p contacts -p kvstore @@ -186,8 +186,8 @@ jobs: - name: Build calculator cdylib (debug) run: cargo build -p calculator - - name: Generate bindings from calculator IDL - run: ./target/release/weaveffi.exe generate samples/calculator/calculator.yml -o generated + - name: Generate bindings from calculator source + run: ./target/release/weaveffi.exe generate samples/calculator/src/lib.rs -o generated - name: Verify generated bindings shell: pwsh @@ -240,8 +240,8 @@ jobs: - name: Generate Android + C bindings for contacts run: | - target/release/weaveffi generate samples/contacts/contacts.yml -o generated --target c - target/release/weaveffi generate samples/contacts/contacts.yml -o generated --target android + target/release/weaveffi generate samples/contacts/src/lib.rs -o generated --target c + target/release/weaveffi generate samples/contacts/src/lib.rs -o generated --target android - name: Compile JNI shim with NDK clang env: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5444914d..2e8ec257 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,10 +77,10 @@ cargo test -p weaveffi-ir ## Adding a new generator Each target language is implemented as its own crate (`weaveffi-gen-`) -that implements the `Generator` trait from `weaveffi_core::codegen`. Before -starting, read the [architecture overview](docs/src/architecture.md) for the -crate dependency graph, the IDL → IR → Validate → Resolve → Generate → Output -data flow, and the snapshot-test layout new generators must hook into. +that implements the `LanguageBackend` trait from `weaveffi_core::backend`. +Before starting, read the [architecture overview](docs/src/architecture.md) +for the crate dependency graph, the Parse → Validate → Configure → Generate → +Output data flow, and the snapshot-test layout new generators must hook into. The short version: @@ -91,17 +91,16 @@ The short version: associated `Config` type, then `name`, `prefix` (if the config carries a `c_prefix`), and `files`. For a single-pass layout, override the `render_enum`/`render_struct`/`render_function` hooks and compose - `emit_members`; otherwise build the layout directly in `files`. Add - `weaveffi_core::impl_generator_via_backend!();` to bridge it to - `Generator`. Reuse `BindingModel` and the shared - `weaveffi_core::codegen::common` helpers instead of re-deriving traversal - or ABI classification. -3. Wire the generator into `crates/weaveffi-cli/src/main.rs` so the - `--targets` flag accepts it. -4. Add snapshot fixtures under - `crates/weaveffi-cli/tests/snapshots.rs` covering at minimum the - `calculator`, `contacts`, `inventory`, `async_demo`, and `events` sample - IDLs. + `emit_members`; otherwise build the layout directly in `files`. Reuse + `BindingModel`, `Ty::family()`, and `Ty::wire()` instead of re-deriving + traversal or ABI classification. +3. Add one line to the `cli_targets!` registry in + `crates/weaveffi-cli/src/config.rs`; that wires the `--target` token, + the `[generators.]` table in `weaveffi.toml`, and the orchestrator + registration at once. +4. Add the target to the `snapshot_tests!` invocation in + `crates/weaveffi-cli/tests/snapshots.rs` and to the determinism test in + `tests/determinism.rs`; every fixture in the corpus then runs against it. 5. Document the generator under `docs/src/generators/.md` and link it from `docs/src/SUMMARY.md`. diff --git a/Cargo.lock b/Cargo.lock index c766265b..4c2ba98c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,7 +68,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -79,7 +79,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -146,12 +146,6 @@ dependencies = [ "backtrace", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.13.1" @@ -297,16 +291,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" -[[package]] -name = "clap_mangen" -version = "0.2.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e30ffc187e2e3aeafcd1c6e2aa416e29739454c0ccaa419226d5ecd181f2d78" -dependencies = [ - "clap", - "roff", -] - [[package]] name = "colorchoice" version = "1.0.5" @@ -321,7 +305,7 @@ checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -385,15 +369,6 @@ dependencies = [ "itertools", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.7" @@ -488,7 +463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -504,16 +479,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - [[package]] name = "find-msvc-tools" version = "0.1.11" @@ -529,15 +494,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - [[package]] name = "futures" version = "0.3.34" @@ -709,26 +665,6 @@ dependencies = [ "hashbrown 0.17.1", ] -[[package]] -name = "inotify" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" -dependencies = [ - "bitflags 1.3.2", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" -dependencies = [ - "libc", -] - [[package]] name = "insta" version = "1.48.0" @@ -757,7 +693,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -808,26 +744,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "kqueue" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" -dependencies = [ - "bitflags 2.13.1", - "libc", -] - [[package]] name = "kvstore" version = "0.1.0" @@ -943,25 +859,13 @@ dependencies = [ "adler2", ] -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - [[package]] name = "napi" version = "3.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58c5f4d5375213fdb7be2655e152386e82f026f9a5ba36a75556e11359aafe09" dependencies = [ - "bitflags 2.13.1", + "bitflags", "ctor", "futures", "libc", @@ -1025,32 +929,13 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" -[[package]] -name = "notify" -version = "6.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" -dependencies = [ - "bitflags 2.13.1", - "crossbeam-channel", - "filetime", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "walkdir", - "windows-sys 0.48.0", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -1232,12 +1117,6 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" -[[package]] -name = "roff" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189" - [[package]] name = "rustc-demangle" version = "0.1.28" @@ -1256,11 +1135,11 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.1", + "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -1501,7 +1380,7 @@ dependencies = [ "getrandom", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -1511,7 +1390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -1738,12 +1617,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - [[package]] name = "wasm-bindgen" version = "0.2.127" @@ -1794,6 +1667,7 @@ name = "weaveffi" version = "0.21.0" dependencies = [ "weaveffi-abi", + "weaveffi-core", "weaveffi-macros", ] @@ -1817,11 +1691,9 @@ dependencies = [ "camino", "clap", "clap_complete", - "clap_mangen", "criterion", "insta", "miette", - "notify", "predicates", "schemars", "serde", @@ -1881,10 +1753,7 @@ version = "0.21.0" dependencies = [ "camino", "serde", - "tempfile", "weaveffi-core", - "weaveffi-gen-c", - "weaveffi-ir", ] [[package]] @@ -1905,7 +1774,6 @@ dependencies = [ "heck", "serde", "weaveffi-core", - "weaveffi-ir", ] [[package]] @@ -1916,7 +1784,6 @@ dependencies = [ "heck", "serde", "weaveffi-core", - "weaveffi-ir", ] [[package]] @@ -1926,9 +1793,7 @@ dependencies = [ "camino", "heck", "serde", - "tempfile", "weaveffi-core", - "weaveffi-ir", ] [[package]] @@ -1939,7 +1804,6 @@ dependencies = [ "heck", "serde", "weaveffi-core", - "weaveffi-ir", ] [[package]] @@ -1949,9 +1813,7 @@ dependencies = [ "camino", "heck", "serde", - "tempfile", "weaveffi-core", - "weaveffi-ir", ] [[package]] @@ -1961,9 +1823,7 @@ dependencies = [ "camino", "heck", "serde", - "tempfile", "weaveffi-core", - "weaveffi-ir", ] [[package]] @@ -1973,9 +1833,7 @@ dependencies = [ "camino", "heck", "serde", - "tempfile", "weaveffi-core", - "weaveffi-ir", ] [[package]] @@ -2053,7 +1911,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -2062,15 +1920,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -2080,63 +1929,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "winnow" version = "0.7.15" diff --git a/README.md b/README.md index 79b7eb80..05b056d6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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.7.0-orange)](./weaveffi.schema.json) [![downloads](https://img.shields.io/crates/d/weaveffi-cli.svg)](https://crates.io/crates/weaveffi-cli) +[![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.8.0-orange)](./weaveffi.schema.json) [![downloads](https://img.shields.io/crates/d/weaveffi-cli.svg)](https://crates.io/crates/weaveffi-cli) WeaveFFI generates type-safe bindings for 11 languages for any native library that exposes a C ABI, whether it's written in Rust, C, C++, Zig, or anything @@ -29,7 +29,7 @@ declares. An interface is a real object with methods; an error domain plus `throws: true` gives its fallible members typed errors: ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: kv errors: @@ -260,33 +260,55 @@ Verify the install: ```bash weaveffi --version -weaveffi schema-version # prints 0.7.0 +weaveffi schema-version # prints 0.8.0 ``` ## CLI reference | Command | Description | |---------|-------------| -| `weaveffi new ` | Scaffold a new project with a starter IDL and `Cargo.toml` | -| `weaveffi generate -o ` | Generate bindings from annotated Rust (`.rs`) or an IDL (`.yml`/`.json`/`.toml`); `--target c,swift,...` to subset, `--config cfg.toml` for options, `--scaffold` to emit Rust FFI stubs (for non-macro producers), `--dry-run` to preview | +| `weaveffi generate -o ` | Generate bindings from annotated Rust (`.rs`) or an IDL (`.yml`/`.json`/`.toml`); `--target c,swift,...` to subset, `--config weaveffi.toml` to override the auto-discovered project config, `--dry-run` to preview, `--force` to bypass the output cache | | `weaveffi package -o ` | Assemble publishable, per-platform packages that bundle a prebuilt native library; `--binaries ` for prebuilt libs or `--build ` to cross-compile a Rust producer | -| `weaveffi validate ` | Validate an IDL definition without generating; `--format json` for machine-readable output | -| `weaveffi lint ` | Lint an IDL and report non-fatal warnings | +| `weaveffi validate ` | Validate an API definition without generating; `--warn` to also report advisory lints, `--format json` for machine-readable output | | `weaveffi diff ` | Show what would change if bindings were regenerated; `--check` for CI | -| `weaveffi extract ` | Derive an IDL from `#[weaveffi::module]`-annotated Rust source | -| `weaveffi format ` | Rewrite an IDL file in canonical form (sorted keys); `--check` for CI | -| `weaveffi watch ` | Re-run `generate` whenever the IDL file changes | +| `weaveffi extract ` | Derive an IDL document from `#[weaveffi::module]`-annotated Rust source | | `weaveffi schema --format json-schema` | Print the JSON Schema for the IDL | -| `weaveffi schema-version` | Print the current IR schema version (`0.7.0`) | -| `weaveffi doctor` | Check for required toolchains; `--target swift` to scope to one language, `--format json` for CI | +| `weaveffi schema-version` | Print the current IDL schema version (`0.8.0`) | | `weaveffi completions ` | Print shell completion scripts (`bash`, `zsh`, `fish`, `powershell`, `elvish`) | -| `weaveffi man --out ` | Generate roff man pages for the CLI to the specified directory | + +### Project configuration: `weaveffi.toml` + +The API definition describes only the API. Everything about how it's +published lives in an optional `weaveffi.toml` next to it (the CLI looks for +the nearest one at or above the input file; `--config` points at a specific +one): + +```toml +[package] +name = "kvstore" # stamped into every generated manifest +version = "0.1.0" +description = "An embedded key-value store" +license = "MIT" + +[global] +c_prefix = "kv" # rename the C ABI symbol prefix for every target + +[generators.swift] +module_name = "KVStore" + +[generators.node] +strip_module_prefix = true +``` + +Without a `weaveffi.toml`, a Rust producer's package name defaults to its +crate directory name, and every generator runs with its defaults. See +[Configuration](docs/src/guides/config.md) for every key. Reference the JSON Schema from your IDL for editor autocompletion: ```yaml # yaml-language-server: $schema=./weaveffi.schema.json -version: "0.7.0" +version: "0.8.0" modules: ... ``` diff --git a/conformance/run.sh b/conformance/run.sh index 15f2c84f..9a70bf8a 100755 --- a/conformance/run.sh +++ b/conformance/run.sh @@ -5,7 +5,8 @@ # asserts concrete results, then exits non-zero on any mismatch. The harness: # # 1. builds each producer cdylib from samples/, -# 2. runs `weaveffi generate` for that sample into the cargo target dir, and +# 2. runs `weaveffi generate` on that producer's annotated `src/lib.rs` (the +# single source of truth for its API) into the cargo target dir, and # 3. compiles + runs every per-(language, sample) consumer, requiring exit 0. # # This is the regression oracle for the code generators: a backend that lowers a @@ -81,7 +82,7 @@ build_producer() { # invocations (e.g. per-language lanes running in parallel) share one # pre-generated tree instead of racing on the rm -rf below. generate() { - local sample=$1 idl=$2 + local sample=$1 idl=${2:-samples/$1/src/lib.rs} if [ -n "${SKIP_GEN:-}" ]; then echo "--- reusing bindings: $sample" return 0 @@ -504,17 +505,17 @@ wasm_async_demo() { wasm_consumer async-demo async_demo.mjs; } # Producers + generation # --------------------------------------------------------------------------- build_producer contacts -generate contacts samples/contacts/contacts.yml +generate contacts build_producer events -generate events samples/events/events.yml +generate events build_producer kvstore -generate kvstore samples/kvstore/kvstore.yml +generate kvstore build_producer shapes -generate shapes samples/shapes/shapes.yml +generate shapes build_producer async-demo -generate async-demo samples/async-demo/async_demo.yml +generate async-demo # Header-only: the producer-export lane implements this in C, no Rust cdylib. -generate calculator samples/calculator/calculator.yml +generate calculator # --------------------------------------------------------------------------- # Run matrix: every language runs the events lane (callbacks/listeners + diff --git a/crates/weaveffi-abi/src/lib.rs b/crates/weaveffi-abi/src/lib.rs index 943225d2..89703ed0 100644 --- a/crates/weaveffi-abi/src/lib.rs +++ b/crates/weaveffi-abi/src/lib.rs @@ -22,6 +22,27 @@ use std::os::raw::c_char; use std::ptr; use std::sync::atomic::{AtomicBool, Ordering}; +/// The revision of the WeaveFFI C ABI this runtime implements. +/// +/// Every producer cdylib exports it as `weaveffi_abi_version()` (via +/// [`export_runtime!`]), and every generated consumer embeds the revision it +/// was generated against. Consumers that can do so cheaply compare the two +/// at load time and refuse to run against a producer built for a different +/// revision, turning a silent memory-layout mismatch into a clear error. +/// +/// The number only changes when the runtime surface (the `weaveffi_error` +/// layout, the value-buffer encoding, or the set and signatures of the +/// `weaveffi_*` runtime symbols) changes incompatibly. It is independent of +/// the crate version and of the IDL schema version. +pub const ABI_VERSION: u32 = 1; + +/// Return [`ABI_VERSION`]; the body behind the exported +/// `weaveffi_abi_version` thunk. +#[must_use] +pub const fn abi_version() -> u32 { + ABI_VERSION +} + /// Public opaque handle type exposed to foreign callers. pub type weaveffi_handle_t = u64; diff --git a/crates/weaveffi-abi/src/macros.rs b/crates/weaveffi-abi/src/macros.rs index c55ccbe5..af8e96e9 100644 --- a/crates/weaveffi-abi/src/macros.rs +++ b/crates/weaveffi-abi/src/macros.rs @@ -19,6 +19,7 @@ /// The macro expands to a fixed set of functions named with the /// `weaveffi_` prefix: /// +/// - `weaveffi_abi_version` /// - `weaveffi_free_string` /// - `weaveffi_free_bytes` /// - `weaveffi_error_clear` @@ -53,6 +54,14 @@ #[macro_export] macro_rules! export_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. + #[no_mangle] + pub extern "C" fn weaveffi_abi_version() -> u32 { + $crate::abi_version() + } + #[no_mangle] pub extern "C" fn weaveffi_free_string(ptr: *const ::std::os::raw::c_char) { $crate::free_string(ptr) diff --git a/crates/weaveffi-bridge/src/lib.rs b/crates/weaveffi-bridge/src/lib.rs index fba8c658..bb2e39a2 100644 --- a/crates/weaveffi-bridge/src/lib.rs +++ b/crates/weaveffi-bridge/src/lib.rs @@ -554,6 +554,7 @@ fn extract_struct(item: &syn::ItemStruct) -> syn::Result { Ok(StructDef { name, doc: extract_doc(&item.attrs), + deprecated: parse_deprecated(&item.attrs).1, fields, }) } @@ -648,6 +649,7 @@ fn extract_enum(item: &syn::ItemEnum) -> syn::Result { Ok(EnumDef { name, doc: extract_doc(&item.attrs), + deprecated: parse_deprecated(&item.attrs).1, variants, }) } @@ -749,6 +751,7 @@ pub fn module_from_item_mod(item_mod: &syn::ItemMod) -> syn::Result { interfaces.push(InterfaceDef { name: s.ident.to_string(), doc: extract_doc(&s.attrs), + deprecated: parse_deprecated(&s.attrs).1, constructors: vec![], methods: vec![], statics: vec![], @@ -798,6 +801,7 @@ pub fn module_from_item_mod(item_mod: &syn::ItemMod) -> syn::Result { Ok(Module { name, + doc: extract_doc(&item_mod.attrs), functions, interfaces, structs, @@ -827,8 +831,6 @@ pub fn api_from_file(file: &syn::File) -> syn::Result { Ok(Api { version: CURRENT_SCHEMA_VERSION.to_string(), modules, - generators: None, - package: None, }) } diff --git a/crates/weaveffi-cli/Cargo.toml b/crates/weaveffi-cli/Cargo.toml index d820045b..e93fd180 100644 --- a/crates/weaveffi-cli/Cargo.toml +++ b/crates/weaveffi-cli/Cargo.toml @@ -43,9 +43,7 @@ serde_yaml = { workspace = true } serde_json = { workspace = true } similar = { workspace = true } tempfile = { workspace = true } -notify = { workspace = true } schemars = { workspace = true } -clap_mangen = { workspace = true } [dev-dependencies] assert_cmd = "2" diff --git a/crates/weaveffi-cli/benches/generate_bench.rs b/crates/weaveffi-cli/benches/generate_bench.rs index 4f6700b1..c56cb3d5 100644 --- a/crates/weaveffi-cli/benches/generate_bench.rs +++ b/crates/weaveffi-cli/benches/generate_bench.rs @@ -2,7 +2,7 @@ use std::path::Path; use camino::Utf8Path; use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use weaveffi_core::codegen::{ConfiguredGenerator, DynGenerator, Generator, Orchestrator}; +use weaveffi_core::codegen::{ConfiguredBackend, Orchestrator, Target}; use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::validate::validate_api; use weaveffi_gen_android::{AndroidConfig, AndroidGenerator}; @@ -21,6 +21,32 @@ use weaveffi_ir::ir::{ }; use weaveffi_ir::parse::parse_api_str; +fn field(name: &str, ty: TypeRef) -> StructField { + StructField { + name: name.into(), + ty, + doc: None, + } +} + +fn param(name: &str, ty: TypeRef) -> Param { + Param { + name: name.into(), + ty, + mutable: false, + doc: None, + } +} + +fn variant(name: &str, value: i32) -> EnumVariant { + EnumVariant { + name: name.into(), + value, + doc: None, + fields: vec![], + } +} + /// 10 modules x (50 functions + 5 structs + 3 enums) each. Type names are /// namespaced per module (`M0Struct0`, ...) because bare type names must be /// unique across the whole API. @@ -31,22 +57,11 @@ fn build_large_api() -> ResolvedApi { .map(|s| StructDef { name: format!("M{m}Struct{s}"), doc: None, + deprecated: None, fields: vec![ - StructField { - name: "id".into(), - ty: TypeRef::I32, - doc: None, - }, - StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "active".into(), - ty: TypeRef::Bool, - doc: None, - }, + field("id", TypeRef::I32), + field("name", TypeRef::StringUtf8), + field("active", TypeRef::Bool), ], }) .collect(); @@ -55,26 +70,8 @@ fn build_large_api() -> ResolvedApi { .map(|e| EnumDef { name: format!("M{m}Enum{e}"), doc: None, - variants: vec![ - EnumVariant { - name: "Alpha".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Beta".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Gamma".into(), - value: 2, - doc: None, - fields: vec![], - }, - ], + deprecated: None, + variants: vec![variant("Alpha", 0), variant("Beta", 1), variant("Gamma", 2)], }) .collect(); @@ -83,29 +80,11 @@ fn build_large_api() -> ResolvedApi { name: format!("func{f}"), doc: Some(format!("Function {f} in module {m}")), params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }, - Param { - name: "c".into(), - // The synthetic API skips the resolve pass, so - // user-type references use the resolved `Record` - // form the generators require. - ty: TypeRef::Record(format!("M{m}Struct0")), - mutable: false, - doc: None, - }, + param("a", TypeRef::I32), + param("b", TypeRef::StringUtf8), + param("c", TypeRef::Named(format!("M{m}Struct0"))), ], - returns: Some(TypeRef::Optional(Box::new(TypeRef::Record(format!( + returns: Some(TypeRef::Optional(Box::new(TypeRef::Named(format!( "M{m}Struct1" ))))), throws: false, @@ -118,6 +97,7 @@ fn build_large_api() -> ResolvedApi { Module { name: format!("mod{m}"), + doc: None, functions, interfaces: vec![], structs, @@ -130,103 +110,86 @@ fn build_large_api() -> ResolvedApi { }) .collect(); - // The synthetic API already uses resolved type references (see the - // `Record` comment above), so it skips the validate/resolve pass. - ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules, - generators: None, - package: None, - }) + validate_api( + Api { + version: "0.8.0".into(), + modules, + }, + None, + ) + .expect("synthetic API validates") } -/// Construct the same fan-out of generators used by the CLI, each with its -/// default per-target config. -fn all_default_generators() -> Vec> { +/// The same fan-out of targets the CLI drives, each with its default config. +fn all_default_targets() -> Vec> { vec![ - Box::new(ConfiguredGenerator::new(CGenerator, CConfig::default())), - Box::new(ConfiguredGenerator::new(CppGenerator, CppConfig::default())), - Box::new(ConfiguredGenerator::new( + Box::new(ConfiguredBackend::new(CGenerator, CConfig::default())), + Box::new(ConfiguredBackend::new(CppGenerator, CppConfig::default())), + Box::new(ConfiguredBackend::new( SwiftGenerator, SwiftConfig::default(), )), - Box::new(ConfiguredGenerator::new( + Box::new(ConfiguredBackend::new( AndroidGenerator, AndroidConfig::default(), )), - Box::new(ConfiguredGenerator::new( - NodeGenerator, - NodeConfig::default(), - )), - Box::new(ConfiguredGenerator::new( - WasmGenerator, - WasmConfig::default(), - )), - Box::new(ConfiguredGenerator::new( + Box::new(ConfiguredBackend::new(NodeGenerator, NodeConfig::default())), + Box::new(ConfiguredBackend::new(WasmGenerator, WasmConfig::default())), + Box::new(ConfiguredBackend::new( PythonGenerator, PythonConfig::default(), )), - Box::new(ConfiguredGenerator::new( + Box::new(ConfiguredBackend::new( DotnetGenerator, DotnetConfig::default(), )), - Box::new(ConfiguredGenerator::new( - DartGenerator, - DartConfig::default(), - )), - Box::new(ConfiguredGenerator::new(GoGenerator, GoConfig::default())), - Box::new(ConfiguredGenerator::new( - RubyGenerator, - RubyConfig::default(), - )), + Box::new(ConfiguredBackend::new(DartGenerator, DartConfig::default())), + Box::new(ConfiguredBackend::new(GoGenerator, GoConfig::default())), + Box::new(ConfiguredBackend::new(RubyGenerator, RubyConfig::default())), ] } +fn run_all(targets: &[Box], api: &ResolvedApi) { + let mut orchestrator = Orchestrator::new(); + for t in targets { + orchestrator = orchestrator.with_target(t.as_ref()); + } + let dir = tempfile::tempdir().unwrap(); + let out = Utf8Path::from_path(dir.path()).unwrap(); + orchestrator + .run(black_box(api), out, &Default::default(), true) + .unwrap(); +} + fn bench_generate_c_large_api(c: &mut Criterion) { let api = build_large_api(); - let gen = CGenerator; let cfg = CConfig::default(); - c.bench_function("generate_c_large_api", |b| { b.iter(|| { let dir = tempfile::tempdir().unwrap(); let out = Utf8Path::from_path(dir.path()).unwrap(); - gen.generate(black_box(&api), out, &cfg).unwrap(); + weaveffi_core::backend::run(&CGenerator, black_box(&api), out, &cfg).unwrap(); }); }); } fn bench_generate_swift_large_api(c: &mut Criterion) { let api = build_large_api(); - let gen = SwiftGenerator; let cfg = SwiftConfig::default(); - c.bench_function("generate_swift_large_api", |b| { b.iter(|| { let dir = tempfile::tempdir().unwrap(); let out = Utf8Path::from_path(dir.path()).unwrap(); - gen.generate(black_box(&api), out, &cfg).unwrap(); + weaveffi_core::backend::run(&SwiftGenerator, black_box(&api), out, &cfg).unwrap(); }); }); } fn bench_generate_all_large_api(c: &mut Criterion) { let api = build_large_api(); - let generators = all_default_generators(); - - let mut orchestrator = Orchestrator::new(); - for g in &generators { - orchestrator = orchestrator.with_generator(g.as_ref()); - } - + let targets = all_default_targets(); c.bench_function("generate_all_large_api", |b| { - b.iter(|| { - let dir = tempfile::tempdir().unwrap(); - let out = Utf8Path::from_path(dir.path()).unwrap(); - orchestrator - .run(black_box(&api), out, &Default::default(), true) - .unwrap(); - }); + b.iter(|| run_all(&targets, &api)); }); } @@ -234,7 +197,7 @@ fn bench_generate_all_large_api(c: &mut Criterion) { /// parallel-vs-serial benchmark exercises every generator against a /// realistic, full-featured API. fn load_kitchen_sink_api() -> ResolvedApi { - let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/06_kitchen_sink.yml"); + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/kitchen_sink.yml"); let contents = std::fs::read_to_string(&path) .unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display())); let api = parse_api_str(&contents, "yaml") @@ -242,72 +205,48 @@ fn load_kitchen_sink_api() -> ResolvedApi { validate_api(api, None).unwrap_or_else(|e| panic!("validate fixture {}: {e}", path.display())) } -/// Parse and validate the calculator sample IDL. +/// Extract and validate the calculator sample from its annotated Rust source. fn load_calculator_api() -> ResolvedApi { - let path = - Path::new(env!("CARGO_MANIFEST_DIR")).join("../../samples/calculator/calculator.yml"); + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../samples/calculator/src/lib.rs"); let contents = std::fs::read_to_string(&path) .unwrap_or_else(|e| panic!("read sample {}: {e}", path.display())); - let api = parse_api_str(&contents, "yaml") - .unwrap_or_else(|e| panic!("parse sample {}: {e}", path.display())); + let api = weaveffi_bridge::api_from_src_stringly(&contents) + .unwrap_or_else(|e| panic!("extract sample {}: {e}", path.display())); validate_api(api, None).unwrap_or_else(|e| panic!("validate sample {}: {e}", path.display())) } -fn run_all_generators(api: &ResolvedApi) { - let generators = all_default_generators(); - let mut orchestrator = Orchestrator::new(); - for g in &generators { - orchestrator = orchestrator.with_generator(g.as_ref()); - } - - let dir = tempfile::tempdir().unwrap(); - let out = Utf8Path::from_path(dir.path()).unwrap(); - orchestrator - .run(black_box(api), out, &Default::default(), true) - .unwrap(); -} - /// Target: full codegen (all 11 generators) < 500ms for the calculator sample. fn bench_full_codegen_calculator(c: &mut Criterion) { let api = load_calculator_api(); + let targets = all_default_targets(); c.bench_function("full_codegen_calculator", |b| { - b.iter(|| run_all_generators(&api)); + b.iter(|| run_all(&targets, &api)); }); } /// Target: full codegen (all 11 generators) < 2000ms for the kitchen-sink fixture. fn bench_full_codegen_kitchen_sink(c: &mut Criterion) { let api = load_kitchen_sink_api(); + let targets = all_default_targets(); c.bench_function("full_codegen_kitchen_sink", |b| { - b.iter(|| run_all_generators(&api)); + b.iter(|| run_all(&targets, &api)); }); } fn bench_generate_all_parallel_vs_serial(c: &mut Criterion) { let api = load_kitchen_sink_api(); - let generators = all_default_generators(); - - let mut orchestrator = Orchestrator::new(); - for g in &generators { - orchestrator = orchestrator.with_generator(g.as_ref()); - } + let targets = all_default_targets(); let mut group = c.benchmark_group("generate_all_kitchen_sink"); group.bench_function("parallel", |b| { - b.iter(|| { - let dir = tempfile::tempdir().unwrap(); - let out = Utf8Path::from_path(dir.path()).unwrap(); - orchestrator - .run(black_box(&api), out, &Default::default(), true) - .unwrap(); - }); + b.iter(|| run_all(&targets, &api)); }); group.bench_function("serial", |b| { b.iter(|| { let dir = tempfile::tempdir().unwrap(); let out = Utf8Path::from_path(dir.path()).unwrap(); - for g in &generators { - g.generate(black_box(&api), out).unwrap(); + for t in &targets { + t.generate(black_box(&api), out).unwrap(); } }); }); diff --git a/crates/weaveffi-cli/src/commands/diff.rs b/crates/weaveffi-cli/src/commands/diff.rs index b8dad7cc..affb4170 100644 --- a/crates/weaveffi-cli/src/commands/diff.rs +++ b/crates/weaveffi-cli/src/commands/diff.rs @@ -1,18 +1,22 @@ //! `weaveffi diff`: regenerate into a temp directory and compare against //! the on-disk output (`--check` for CI gating with distinct exit codes). -use crate::config::{merge_inline_generators, CliConfig}; use camino::Utf8Path; use miette::{miette, IntoDiagnostic, Result, WrapErr}; use similar::TextDiff; use std::collections::BTreeSet; use weaveffi_core::codegen::Orchestrator; -pub(crate) fn cmd_diff(input: &str, out: Option<&str>, check: bool, quiet: bool) -> Result<()> { +pub(crate) fn cmd_diff( + input: &str, + out: Option<&str>, + config_path: Option<&str>, + check: bool, + quiet: bool, +) -> Result<()> { let out = out.unwrap_or("./generated"); - let in_path = Utf8Path::new(input); - let (api, _contents) = super::load_validated_api(input)?; + let (config, api) = super::load_project(input, config_path, false)?; let tmp = tempfile::tempdir() .into_diagnostic() @@ -20,17 +24,12 @@ pub(crate) fn cmd_diff(input: &str, out: Option<&str>, check: bool, quiet: bool) let tmp_path = Utf8Path::from_path(tmp.path()) .ok_or_else(|| miette!("temp directory path is not valid UTF-8"))?; - let mut config = CliConfig::default(); - if let Some(ref generators) = api.generators { - merge_inline_generators(&mut config, generators); - } - config.finalize(in_path.file_name().map(str::to_string)); let hooks = config.hooks(); - let generators = config.build_generators(); + let targets = config.select_targets(None)?; let mut orchestrator = Orchestrator::new(); - for gen in &generators { - orchestrator = orchestrator.with_generator(gen.as_ref()); + for target in &targets { + orchestrator = orchestrator.with_target(target.as_ref()); } orchestrator .run(&api, tmp_path, &hooks, true) @@ -152,7 +151,7 @@ mod tests { std::fs::write( &yml, concat!( - "version: \"0.7.0\"\n", + "version: \"0.8.0\"\n", "modules:\n", " - name: math\n", " functions:\n", diff --git a/crates/weaveffi-cli/src/commands/format.rs b/crates/weaveffi-cli/src/commands/format.rs deleted file mode 100644 index d4df5d25..00000000 --- a/crates/weaveffi-cli/src/commands/format.rs +++ /dev/null @@ -1,134 +0,0 @@ -//! `weaveffi format`: rewrite an IDL file in canonical form (sorted keys, -//! defaults omitted), with `--check` for CI. - -use camino::Utf8Path; -use miette::{IntoDiagnostic, Result, WrapErr}; - -pub(crate) fn cmd_format(input: &str, check: bool, quiet: bool) -> Result<()> { - let in_path = Utf8Path::new(input); - let format = super::input_format(in_path)?; - let (api, contents) = super::load_validated_api(input)?; - - let formatted = match format { - "yaml" => format_api_yaml(&api)?, - "json" => format_api_json(&api)?, - "toml" => toml::to_string_pretty(api.api()) - .into_diagnostic() - .wrap_err("failed to serialize API as TOML")?, - _ => unreachable!(), - }; - - if check { - if formatted != contents { - if !quiet { - eprintln!( - "{input} is not canonically formatted; run 'weaveffi format {input}' to fix" - ); - } - std::process::exit(1); - } - if !quiet { - println!("{input} is canonically formatted"); - } - } else { - if formatted == contents { - if !quiet { - println!("{input} is already canonically formatted"); - } - return Ok(()); - } - std::fs::write(in_path.as_std_path(), &formatted) - .into_diagnostic() - .wrap_err_with(|| format!("failed to write {}", input))?; - if !quiet { - println!("Formatted {input}"); - } - } - Ok(()) -} - -/// Serialize `api` as YAML with deterministic key ordering. `serde_yaml` -/// preserves the order yielded by the serializer, so we round-trip through -/// `serde_json::Value` whose object representation is a `BTreeMap` and -/// therefore alphabetically sorted at every level. -fn format_api_yaml(api: &weaveffi_ir::ir::Api) -> Result { - let json: serde_json::Value = serde_json::to_value(api) - .into_diagnostic() - .wrap_err("failed to convert API to JSON value")?; - serde_yaml::to_string(&json) - .into_diagnostic() - .wrap_err("failed to serialize API as YAML") -} - -/// Serialize `api` as pretty-printed JSON with sorted keys at every level by -/// going through `serde_json::Value` (whose `Object` is a `BTreeMap`). -fn format_api_json(api: &weaveffi_ir::ir::Api) -> Result { - let json: serde_json::Value = serde_json::to_value(api) - .into_diagnostic() - .wrap_err("failed to convert API to JSON value")?; - let mut out = serde_json::to_string_pretty(&json) - .into_diagnostic() - .wrap_err("failed to serialize API as JSON")?; - out.push('\n'); - Ok(out) -} - -#[cfg(test)] -mod tests { - #[test] - fn format_is_idempotent_and_omits_defaults() { - let dir = tempfile::tempdir().unwrap(); - let sample_src = format!( - "{}/../../samples/calculator/calculator.yml", - env!("CARGO_MANIFEST_DIR") - ); - let target = dir.path().join("calc.yml"); - std::fs::copy(&sample_src, &target).unwrap(); - let target_str = target.to_str().unwrap(); - - // First format establishes the canonical form. - let out = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args(["--quiet", "format", target_str]) - .output() - .expect("failed to run weaveffi format"); - assert!( - out.status.success(), - "format failed: {}", - String::from_utf8_lossy(&out.stderr) - ); - let formatted = std::fs::read_to_string(&target).unwrap(); - - // Defaulted fields must not leak into the canonical IDL. - for needle in ["null", "async:", "cancellable:", "mutable:", ": []"] { - assert!( - !formatted.contains(needle), - "canonical IDL should omit default `{needle}`:\n{formatted}" - ); - } - - // `--check` now passes on the canonical file (the defect being fixed). - let check = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args(["format", target_str, "--check"]) - .output() - .expect("failed to run weaveffi format --check"); - assert!( - check.status.success(), - "format --check should pass on canonical file: {}", - String::from_utf8_lossy(&check.stderr) - ); - - // Formatting again is a no-op (idempotent). - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args(["--quiet", "format", target_str]) - .output() - .expect("failed to run weaveffi format (2nd)"); - assert_eq!( - formatted, - std::fs::read_to_string(&target).unwrap(), - "format must be idempotent" - ); - } -} diff --git a/crates/weaveffi-cli/src/commands/generate.rs b/crates/weaveffi-cli/src/commands/generate.rs index 46f07399..7e925c20 100644 --- a/crates/weaveffi-cli/src/commands/generate.rs +++ b/crates/weaveffi-cli/src/commands/generate.rs @@ -1,56 +1,29 @@ -//! `weaveffi generate`: parse, validate, and run the selected generators -//! through the orchestrator (plus `--scaffold` and `--dry-run`). +//! `weaveffi generate`: parse, validate, and run the selected targets +//! through the orchestrator (plus `--dry-run`). -use crate::config::{merge_inline_generators, CliConfig}; -use crate::scaffold; use camino::Utf8Path; use miette::{miette, IntoDiagnostic, Result, WrapErr}; -use weaveffi_core::codegen::{DynGenerator, Orchestrator}; -use weaveffi_core::validate::collect_warnings; +use weaveffi_core::codegen::Orchestrator; #[allow(clippy::too_many_arguments)] pub(crate) fn cmd_generate( input: &str, out: &str, targets: Option<&str>, - emit_scaffold: bool, config_path: Option<&str>, warn: bool, force: bool, dry_run: bool, quiet: bool, ) -> Result<()> { - let mut config = CliConfig::load(config_path)?; - - let in_path = Utf8Path::new(input); - let (api, _contents) = super::load_validated_api(input)?; - - if let Some(ref generators) = api.generators { - merge_inline_generators(&mut config, generators); - } - config.finalize(in_path.file_name().map(str::to_string)); - - if warn { - for w in collect_warnings(&api) { - eprintln!("warning: {w}"); - } - } - + let (config, api) = super::load_project(input, config_path, warn)?; let out_dir = Utf8Path::new(out); - let scaffold_prefix = config.scaffold_prefix(); let hooks = config.hooks(); - let generators = config.build_generators(); - - let filter: Option> = targets.map(|t| t.split(',').map(str::trim).collect()); - let selected: Vec<&dyn DynGenerator> = generators - .iter() - .map(|g| g.as_ref()) - .filter(|g| filter.as_ref().is_none_or(|ts| ts.contains(&g.name()))) - .collect(); + let selected = config.select_targets(targets)?; if dry_run { - for gen in &selected { - for path in gen.output_files(&api, out_dir) { + for target in &selected { + for path in target.output_files(&api, out_dir) { println!("{path}"); } } @@ -62,27 +35,19 @@ pub(crate) fn cmd_generate( .wrap_err_with(|| format!("failed to create output directory: {}", out))?; let mut orchestrator = Orchestrator::new(); - for gen in &selected { - orchestrator = orchestrator.with_generator(*gen); + for target in &selected { + orchestrator = orchestrator.with_target(target.as_ref()); } orchestrator .run(&api, out_dir, &hooks, force) .map_err(|e| miette!("{:#}", e))?; - if emit_scaffold { - let scaffold_path = out_dir.join("scaffold.rs"); - let contents = scaffold::render_scaffold(&api, &scaffold_prefix); - std::fs::write(scaffold_path.as_std_path(), contents) - .into_diagnostic() - .wrap_err_with(|| format!("failed to write {}", scaffold_path))?; - if !quiet { - println!("Scaffold written to {}", scaffold_path); - } - } - if !quiet { - println!("Generated artifacts in {}", out); + match &config.source { + Some(src) => println!("Generated artifacts in {out} (config: {src})"), + None => println!("Generated artifacts in {out}"), + } } Ok(()) } @@ -90,16 +55,16 @@ pub(crate) fn cmd_generate( #[cfg(test)] mod tests { use super::*; - use camino::Utf8Path; + use crate::config::ProjectConfig; #[test] - fn dry_run_lists_files() { + fn dry_run_lists_files_without_writing() { let dir = tempfile::tempdir().unwrap(); let yml = dir.path().join("api.yml"); std::fs::write( &yml, concat!( - "version: \"0.7.0\"\n", + "version: \"0.8.0\"\n", "modules:\n", " - name: math\n", " functions:\n", @@ -116,81 +81,30 @@ mod tests { let input = yml.to_str().unwrap(); let out_str = out.to_str().unwrap(); - cmd_generate(input, out_str, None, false, None, false, false, true, false).unwrap(); - + cmd_generate(input, out_str, None, None, false, false, true, false).unwrap(); assert!(!out.exists(), "dry-run should not create output directory"); - let api = { - let contents = std::fs::read_to_string(&yml).unwrap(); - let api = weaveffi_ir::parse::parse_api_str(&contents, "yaml").unwrap(); - weaveffi_core::validate::validate_api(api, None).unwrap() - }; + let (_, api) = super::super::load_project(input, None, false).unwrap(); let out_dir = Utf8Path::new(out_str); - - let config = CliConfig::default(); - let generators = config.build_generators(); - - let mut files: Vec = Vec::new(); - for gen in &generators { - files.extend(gen.output_files(&api, out_dir)); + let files: Vec = ProjectConfig::default() + .select_targets(None) + .unwrap() + .iter() + .flat_map(|t| t.output_files(&api, out_dir)) + .collect(); + + for expected in [ + "c/weaveffi.h", + "swift/Package.swift", + "android/build.gradle", + "node/types.d.ts", + "wasm/weaveffi_wasm.js", + "python/weaveffi/__init__.py", + ] { + assert!( + files.iter().any(|f| f.contains(expected)), + "missing {expected}: {files:?}" + ); } - - assert!( - files.iter().any(|f| f.contains("c/weaveffi.h")), - "missing c header: {files:?}" - ); - assert!( - files.iter().any(|f| f.contains("swift/Package.swift")), - "missing swift package: {files:?}" - ); - assert!( - files.iter().any(|f| f.contains("android/build.gradle")), - "missing android gradle: {files:?}" - ); - assert!( - files.iter().any(|f| f.contains("node/types.d.ts")), - "missing node types: {files:?}" - ); - assert!( - files.iter().any(|f| f.contains("wasm/weaveffi_wasm.js")), - "missing wasm js: {files:?}" - ); - assert!( - files - .iter() - .any(|f| f.contains("python/weaveffi/__init__.py")), - "missing python init: {files:?}" - ); - } - - #[test] - fn generate_cpp_target_filter() { - let dir = tempfile::tempdir().unwrap(); - let sample = format!( - "{}/../../samples/calculator/calculator.yml", - env!("CARGO_MANIFEST_DIR") - ); - let out = dir.path().join("out"); - - let cmd = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "generate", - &sample, - "-o", - out.to_str().unwrap(), - "--target", - "cpp", - ]) - .output() - .expect("failed to run weaveffi generate --target cpp"); - - assert!( - cmd.status.success(), - "generate --target cpp failed: {}", - String::from_utf8_lossy(&cmd.stderr) - ); - assert!(out.join("cpp").exists(), "cpp/ should exist in output"); - assert!(!out.join("c").exists(), "c/ should NOT exist in output"); } } diff --git a/crates/weaveffi-cli/src/commands/mod.rs b/crates/weaveffi-cli/src/commands/mod.rs index ba1606d2..c3ec281a 100644 --- a/crates/weaveffi-cli/src/commands/mod.rs +++ b/crates/weaveffi-cli/src/commands/mod.rs @@ -2,17 +2,15 @@ //! dispatch; everything a command does lives here. pub(crate) mod diff; -pub(crate) mod format; pub(crate) mod generate; -pub(crate) mod new; pub(crate) mod package; pub(crate) mod validate; -pub(crate) mod watch; +use crate::config::ProjectConfig; use crate::report::with_named_source; use camino::Utf8Path; use miette::{bail, IntoDiagnostic, Report, Result, WrapErr}; -use weaveffi_core::validate::validate_api; +use weaveffi_core::validate::{collect_warnings, validate_api}; use weaveffi_core::ResolvedApi; use weaveffi_ir::ir::Api; use weaveffi_ir::parse::parse_api_str; @@ -21,14 +19,14 @@ use weaveffi_ir::parse::parse_api_str; pub(crate) fn input_format(in_path: &Utf8Path) -> Result<&'static str> { let ext = in_path.extension().unwrap_or(""); if ext.is_empty() { - bail!("input file has no extension (expected yml|yaml|json|toml)"); + bail!("input file has no extension (expected rs|yml|yaml|json|toml)"); } match ext { "yml" | "yaml" => Ok("yaml"), "json" => Ok("json"), "toml" => Ok("toml"), other => bail!( - "unsupported input format: {} (expected yml|yaml|json|toml)", + "unsupported input format: {} (expected rs|yml|yaml|json|toml)", other ), } @@ -58,11 +56,84 @@ pub(crate) fn load_api(input: &str) -> Result<(Api, String)> { Ok((api, contents)) } -/// [`load_api`] plus validation, the shared front half of `generate`, -/// `lint`, `diff`, and `format`. Returns the [`ResolvedApi`] that every -/// downstream consumer (orchestrator, scaffold, packagers) requires. +/// [`load_api`] plus validation. Returns the [`ResolvedApi`] that every +/// downstream consumer (orchestrator, packagers) requires. pub(crate) fn load_validated_api(input: &str) -> Result<(ResolvedApi, String)> { let (api, contents) = load_api(input)?; let api = validate_api(api, Some((input, &contents))).map_err(Report::new)?; Ok((api, contents)) } + +/// The shared front half of `generate`, `package`, and `diff`: locate and +/// finalize the project config, load and validate the API, attach the +/// `[package]` identity, and optionally print advisory warnings. +pub(crate) fn load_project( + input: &str, + config_path: Option<&str>, + warn: bool, +) -> Result<(ProjectConfig, ResolvedApi)> { + let in_path = Utf8Path::new(input); + let mut config = ProjectConfig::for_input(config_path, in_path)?; + if config.package.name.is_none() { + config.package.name = crate_dir_name(in_path); + } + let (api, _contents) = load_validated_api(input)?; + let api = api.with_package(config.package.clone()); + if warn { + for w in collect_warnings(&api) { + eprintln!("warning: {w}"); + } + } + Ok((config, api)) +} + +/// For a Rust input named `lib.rs`, `main.rs`, or `mod.rs`, the enclosing +/// crate directory's name (skipping a `src/` component), mirroring how Cargo +/// names a package after its directory. That name stands in for +/// `[package] name` when the project config omits it, so `weaveffi generate +/// samples/kvstore/src/lib.rs` produces a `kvstore` package rather than a +/// `lib` one. Any other input keeps the file-stem fallback applied by +/// [`weaveffi_core::pkg::resolve`]. +fn crate_dir_name(input: &Utf8Path) -> Option { + if input.extension() != Some("rs") { + return None; + } + if !matches!(input.file_stem(), Some("lib" | "main" | "mod")) { + return None; + } + let mut dir = input.parent()?; + if dir.file_name() == Some("src") { + dir = dir.parent()?; + } + dir.file_name() + .filter(|n| !n.is_empty() && *n != ".") + .map(str::to_string) + .or_else(|| { + std::env::current_dir() + .ok() + .and_then(|d| d.file_name().map(|n| n.to_string_lossy().into_owned())) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn crate_dir_name_follows_cargo_conventions() { + assert_eq!( + crate_dir_name(Utf8Path::new("samples/kvstore/src/lib.rs")).as_deref(), + Some("kvstore") + ); + assert_eq!( + crate_dir_name(Utf8Path::new("/abs/crates/foo/src/main.rs")).as_deref(), + Some("foo") + ); + assert_eq!( + crate_dir_name(Utf8Path::new("crates/foo/lib.rs")).as_deref(), + Some("foo") + ); + assert_eq!(crate_dir_name(Utf8Path::new("api/kvstore.rs")), None); + assert_eq!(crate_dir_name(Utf8Path::new("api/kvstore.yml")), None); + } +} diff --git a/crates/weaveffi-cli/src/commands/new.rs b/crates/weaveffi-cli/src/commands/new.rs deleted file mode 100644 index 6027f920..00000000 --- a/crates/weaveffi-cli/src/commands/new.rs +++ /dev/null @@ -1,205 +0,0 @@ -//! `weaveffi new` scaffolds a fresh producer project: starter IDL, -//! `Cargo.toml`, `src/lib.rs` (via the scaffold renderer), and a README. - -use crate::scaffold; -use camino::Utf8Path; -use miette::{bail, IntoDiagnostic, Result, WrapErr}; -use weaveffi_core::validate::validate_api; -use weaveffi_ir::parse::parse_api_str; - -pub(crate) fn cmd_new(name: &str, quiet: bool) -> Result<()> { - let project_dir = Utf8Path::new(name); - if project_dir.exists() { - bail!( - "destination '{}' already exists; choose a new name or remove it", - name - ); - } - std::fs::create_dir_all(project_dir.as_std_path()) - .into_diagnostic() - .wrap_err_with(|| format!("failed to create project directory: {}", name))?; - - let module_name = sanitize_module_name(name); - - let idl_path = project_dir.join("weaveffi.yml"); - let idl_contents = format!( - concat!( - "version: \"0.7.0\"\n", - "package:\n", - " name: {name}\n", - " version: \"0.1.0\"\n", - "modules:\n", - " - name: {module}\n", - " structs:\n", - " - name: Item\n", - " fields:\n", - " - {{ name: id, type: i64 }}\n", - " - {{ name: name, type: string }}\n", - " - {{ name: description, type: \"string?\" }}\n", - " functions:\n", - " - name: create_item\n", - " params:\n", - " - {{ name: name, type: string }}\n", - " - {{ name: description, type: \"string?\" }}\n", - " return: handle\n", - " - name: get_item\n", - " params:\n", - " - {{ name: id, type: handle }}\n", - " return: Item\n", - " - name: list_items\n", - " params: []\n", - " return: \"[Item]\"\n", - " - name: delete_item\n", - " params:\n", - " - {{ name: id, type: handle }}\n", - " return: bool\n", - ), - name = name, - module = module_name - ); - std::fs::write(idl_path.as_std_path(), &idl_contents) - .into_diagnostic() - .wrap_err_with(|| format!("failed to write {}", idl_path))?; - - let api = parse_api_str(&idl_contents, "yaml").wrap_err("failed to parse generated IDL")?; - let api = validate_api(api, None).wrap_err("generated IDL failed validation")?; - - let cargo_toml_path = project_dir.join("Cargo.toml"); - let cargo_toml = format!( - concat!( - "[package]\n", - "name = \"{name}\"\n", - "version = \"0.1.0\"\n", - "edition = \"2021\"\n", - "publish = false\n", - "\n", - "[lib]\n", - "crate-type = [\"cdylib\"]\n", - "\n", - "[dependencies]\n", - "weaveffi-abi = \"0.3\"\n", - "\n", - "[lints.rust]\n", - "unsafe_code = \"allow\"\n", - ), - name = name, - ); - std::fs::write(cargo_toml_path.as_std_path(), &cargo_toml) - .into_diagnostic() - .wrap_err_with(|| format!("failed to write {}", cargo_toml_path))?; - - let src_dir = project_dir.join("src"); - std::fs::create_dir_all(src_dir.as_std_path()) - .into_diagnostic() - .wrap_err_with(|| format!("failed to create {}", src_dir))?; - let lib_rs_path = src_dir.join("lib.rs"); - let lib_contents = scaffold::render_scaffold(&api, "weaveffi"); - std::fs::write(lib_rs_path.as_std_path(), &lib_contents) - .into_diagnostic() - .wrap_err_with(|| format!("failed to write {}", lib_rs_path))?; - - let readme_path = project_dir.join("README.md"); - let readme = format!( - concat!( - "# {name}\n\n", - "A WeaveFFI project.\n\n", - "## Getting Started\n\n", - "1. Implement the `todo!()` stubs in `src/lib.rs`.\n", - "2. Build the shared library:\n\n", - " ```sh\n", - " cargo build\n", - " ```\n\n", - "3. Generate foreign-language bindings:\n\n", - " ```sh\n", - " weaveffi generate weaveffi.yml -o generated\n", - " ```\n\n", - "4. Test your library:\n\n", - " ```sh\n", - " cargo test\n", - " ```\n\n", - "5. Use the generated bindings from Swift, Kotlin, Node.js, Python, .NET, or Wasm.\n", - ), - name = name, - ); - std::fs::write(readme_path.as_std_path(), &readme) - .into_diagnostic() - .wrap_err_with(|| format!("failed to write {}", readme_path))?; - - if !quiet { - println!("Initialized WeaveFFI project at {}", project_dir); - println!("Next steps:"); - println!(" cd {name}"); - println!(" # Implement the todo!() stubs in src/lib.rs"); - println!(" cargo build"); - println!(" weaveffi generate weaveffi.yml -o generated"); - } - Ok(()) -} - -fn sanitize_module_name(name: &str) -> String { - let lowered = name.to_lowercase(); - let mut out = String::with_capacity(lowered.len()); - for ch in lowered.chars() { - if ch.is_ascii_alphanumeric() { - out.push(ch); - } else if matches!(ch, '-' | '_' | ' ') { - out.push('_'); - } - } - if out.is_empty() { - String::from("module") - } else { - out - } -} - -#[cfg(test)] -mod tests { - #[test] - fn new_creates_cargo_toml() { - let dir = tempfile::tempdir().unwrap(); - let cmd = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .current_dir(dir.path()) - .args(["new", "test_proj"]) - .output() - .expect("failed to run weaveffi new"); - - assert!( - cmd.status.success(), - "weaveffi new failed: {}", - String::from_utf8_lossy(&cmd.stderr) - ); - let cargo_toml = dir.path().join("test_proj/Cargo.toml"); - assert!(cargo_toml.exists(), "Cargo.toml should exist"); - let contents = std::fs::read_to_string(&cargo_toml).unwrap(); - assert!( - contents.contains("cdylib"), - "Cargo.toml should contain cdylib: {contents}" - ); - } - - #[test] - fn new_creates_lib_rs() { - let dir = tempfile::tempdir().unwrap(); - let cmd = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .current_dir(dir.path()) - .args(["new", "test_proj"]) - .output() - .expect("failed to run weaveffi new"); - - assert!( - cmd.status.success(), - "weaveffi new failed: {}", - String::from_utf8_lossy(&cmd.stderr) - ); - let lib_rs = dir.path().join("test_proj/src/lib.rs"); - assert!(lib_rs.exists(), "src/lib.rs should exist"); - let contents = std::fs::read_to_string(&lib_rs).unwrap(); - assert!( - contents.contains("todo!()"), - "lib.rs should contain todo!() stubs: {contents}" - ); - } -} diff --git a/crates/weaveffi-cli/src/commands/package.rs b/crates/weaveffi-cli/src/commands/package.rs index 7519460a..08c93446 100644 --- a/crates/weaveffi-cli/src/commands/package.rs +++ b/crates/weaveffi-cli/src/commands/package.rs @@ -14,11 +14,9 @@ //! for each platform's Rust target triple. Convenient locally, but each //! target needs its rustup target and a working cross-linker installed. -use crate::config::{merge_inline_generators, CliConfig}; use camino::{Utf8Path, Utf8PathBuf}; use miette::{bail, miette, IntoDiagnostic, Result, WrapErr}; use std::process::{Command, Stdio}; -use weaveffi_core::codegen::DynGenerator; use weaveffi_core::package::{summarize, write_package, PackageContext}; use weaveffi_core::pkg; use weaveffi_core::platform::{BinarySet, Platform}; @@ -55,19 +53,8 @@ pub(crate) fn cmd_package( ), }; - let mut config = CliConfig::load(config_path)?; + let (config, api) = super::load_project(input, config_path, warn)?; let in_path = Utf8Path::new(input); - let (api, _contents) = super::load_validated_api(input)?; - if let Some(ref generators) = api.generators { - merge_inline_generators(&mut config, generators); - } - config.finalize(in_path.file_name().map(str::to_string)); - - if warn { - for w in weaveffi_core::validate::collect_warnings(&api) { - eprintln!("warning: {w}"); - } - } let selected_platforms = parse_platforms(platforms)?; let input_basename = in_path.file_name(); @@ -91,13 +78,7 @@ pub(crate) fn cmd_package( .into_diagnostic() .wrap_err_with(|| format!("failed to create output directory: {out}"))?; - let generators = config.build_generators(); - let filter: Option> = targets.map(|t| t.split(',').map(str::trim).collect()); - let selected: Vec<&dyn DynGenerator> = generators - .iter() - .map(|g| g.as_ref()) - .filter(|g| filter.as_ref().is_none_or(|ts| ts.contains(&g.name()))) - .collect(); + let selected = config.select_targets(targets)?; if selected.is_empty() { bail!("no targets selected to package"); diff --git a/crates/weaveffi-cli/src/commands/validate.rs b/crates/weaveffi-cli/src/commands/validate.rs index 96e7e1f2..14a674e8 100644 --- a/crates/weaveffi-cli/src/commands/validate.rs +++ b/crates/weaveffi-cli/src/commands/validate.rs @@ -1,5 +1,5 @@ -//! `weaveffi validate` and `weaveffi lint`: schema validation with -//! human-readable or `--format json` output, and advisory warnings. +//! `weaveffi validate`: schema validation with human-readable or `--format +//! json` output, plus advisory warnings under `--warn`. use miette::{Report, Result}; use weaveffi_core::validate::{collect_warnings, validate_api, ValidationError, ValidationWarning}; @@ -15,11 +15,11 @@ pub(crate) fn cmd_validate( match validate_api(api, Some((input, &contents))) { Ok(api) => { - if warn && !quiet { - for w in collect_warnings(&api) { - eprintln!("warning: {w}"); - } - } + let warnings = if warn { + collect_warnings(&api) + } else { + Vec::new() + }; let n_modules = api.modules.len(); let n_functions: usize = api.modules.iter().map(|m| m.functions.len()).sum(); let n_structs: usize = api.modules.iter().map(|m| m.structs.len()).sum(); @@ -31,9 +31,13 @@ pub(crate) fn cmd_validate( "functions": n_functions, "structs": n_structs, "enums": n_enums, + "warnings": warnings.iter().map(warning_to_json).collect::>(), }); println!("{json}"); } else if !quiet { + for w in &warnings { + eprintln!("warning: {w}"); + } println!("Validation passed"); println!( " {} modules, {} functions, {} structs, {} enums", @@ -60,39 +64,6 @@ pub(crate) fn cmd_validate( } } -/// Returns `Ok(true)` when the file is clean, `Ok(false)` when warnings were found. -pub(crate) fn cmd_lint(input: &str, format: Option<&str>, quiet: bool) -> Result { - let json_mode = format == Some("json"); - let (api, _contents) = super::load_validated_api(input)?; - - let warnings = collect_warnings(&api); - if json_mode { - let json = serde_json::json!({ - "ok": warnings.is_empty(), - "warnings": warnings - .iter() - .map(warning_to_json) - .collect::>(), - }); - println!("{json}"); - return Ok(warnings.is_empty()); - } - - if warnings.is_empty() { - if !quiet { - println!("No warnings."); - } - Ok(true) - } else { - if !quiet { - for w in &warnings { - eprintln!("warning: {w}"); - } - } - Ok(false) - } -} - /// Stable string code for a [`ValidationError`] variant, used as the `code` /// field in `weaveffi validate --format json` failure output. Keeping the /// codes in lock-step with the variant identifiers makes them ergonomic to @@ -334,14 +305,6 @@ fn warning_to_json(w: &ValidationWarning) -> serde_json::Value { ValidationWarning::AsyncVoidFunction { module, function } => { ("AsyncVoidFunction", format!("{module}::{function}")) } - ValidationWarning::MutableOnValueType { - module, - function, - param, - } => ( - "MutableOnValueType", - format!("{module}::{function}::{param}"), - ), ValidationWarning::DeprecatedFunction { module, function, .. } => ("DeprecatedFunction", format!("{module}::{function}")), @@ -352,20 +315,3 @@ fn warning_to_json(w: &ValidationWarning) -> serde_json::Value { "message": w.to_string(), }) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn lint_clean_file_succeeds() { - let sample = format!( - "{}/../../samples/calculator/calculator.yml", - env!("CARGO_MANIFEST_DIR") - ); - assert!( - cmd_lint(&sample, None, false).unwrap(), - "calculator sample should be lint-clean" - ); - } -} diff --git a/crates/weaveffi-cli/src/commands/watch.rs b/crates/weaveffi-cli/src/commands/watch.rs deleted file mode 100644 index 9b4288f3..00000000 --- a/crates/weaveffi-cli/src/commands/watch.rs +++ /dev/null @@ -1,221 +0,0 @@ -//! `weaveffi watch`: regenerate whenever the IDL file changes, with a -//! debounce window so editor save bursts trigger a single regeneration. - -use super::generate::cmd_generate; -use camino::Utf8Path; -use miette::{bail, miette, IntoDiagnostic, Result, WrapErr}; -use std::process::Command; - -/// Returns `true` when enough time has elapsed since the most recent file -/// system event for the debounce window to have closed and the generator to -/// fire. Pure function so the watch loop can be unit-tested without real -/// timers or `notify` events. -fn debounce_should_fire( - last_event: std::time::Instant, - now: std::time::Instant, - debounce: std::time::Duration, -) -> bool { - now.saturating_duration_since(last_event) >= debounce -} - -pub(crate) fn cmd_watch( - input: &str, - out: &str, - targets: Option<&str>, - config_path: Option<&str>, - quiet: bool, -) -> Result<()> { - use notify::{EventKind, RecursiveMode, Watcher}; - use std::sync::mpsc; - use std::time::{Duration, Instant}; - - let in_path = Utf8Path::new(input); - let abs_input = std::fs::canonicalize(in_path.as_std_path()) - .into_diagnostic() - .wrap_err_with(|| format!("failed to resolve input file: {}", input))?; - let parent = abs_input - .parent() - .ok_or_else(|| miette!("input file has no parent directory: {}", input))? - .to_path_buf(); - - let (tx, rx) = mpsc::channel::>(); - let mut watcher = notify::recommended_watcher(move |res| { - let _ = tx.send(res); - }) - .into_diagnostic() - .wrap_err("failed to create file watcher")?; - watcher - .watch(&parent, RecursiveMode::NonRecursive) - .into_diagnostic() - .wrap_err_with(|| format!("failed to watch directory: {}", parent.display()))?; - - if let Err(e) = cmd_generate( - input, - out, - targets, - false, - config_path, - false, - false, - false, - quiet, - ) { - eprintln!("error: {e:?}"); - } - if !quiet { - println!("Watching..."); - } - - let debounce = Duration::from_millis(500); - let mut pending: Option = None; - loop { - let timeout = match pending { - Some(t) => debounce - .saturating_sub(t.elapsed()) - .max(Duration::from_millis(10)), - None => Duration::from_secs(60), - }; - match rx.recv_timeout(timeout) { - Ok(Ok(event)) => { - if matches!( - event.kind, - EventKind::Modify(_) | EventKind::Create(_) | EventKind::Any - ) && event.paths.iter().any(|p| { - std::fs::canonicalize(p) - .map(|c| c == abs_input) - .unwrap_or(false) - }) { - pending = Some(Instant::now()); - } - } - Ok(Err(e)) => eprintln!("watch error: {e}"), - Err(mpsc::RecvTimeoutError::Timeout) => {} - Err(mpsc::RecvTimeoutError::Disconnected) => { - bail!("file watcher disconnected unexpectedly"); - } - } - if let Some(t) = pending { - if debounce_should_fire(t, Instant::now(), debounce) { - pending = None; - if let Err(e) = cmd_generate( - input, - out, - targets, - false, - config_path, - false, - false, - false, - quiet, - ) { - eprintln!("error: {e:?}"); - } else if !quiet { - let now = chrono_local_time_string(); - println!("Regenerated at {now}"); - } - } - } - } -} - -/// Format a `HH:MM:SS` timestamp from the system clock without pulling in a -/// chrono dependency. Computes hours/minutes/seconds from the seconds-since- -/// epoch, applying the local UTC offset by reading `localtime`'s difference. -fn chrono_local_time_string() -> String { - use std::time::{SystemTime, UNIX_EPOCH}; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - let local_offset = local_utc_offset_seconds(); - let local = now as i64 + local_offset; - let secs = local.rem_euclid(86_400); - let h = secs / 3600; - let m = (secs % 3600) / 60; - let s = secs % 60; - format!("{h:02}:{m:02}:{s:02}") -} - -/// Best-effort local timezone offset in seconds. Falls back to UTC (`0`) on -/// platforms without an obvious way to query the offset; the watch command -/// only uses this for the friendly "Regenerated at HH:MM:SS" line. -fn local_utc_offset_seconds() -> i64 { - if let Ok(out) = Command::new("date").arg("+%z").output() { - if out.status.success() { - let s = String::from_utf8_lossy(&out.stdout).trim().to_string(); - if s.len() == 5 { - let sign = if s.starts_with('-') { -1 } else { 1 }; - if let (Ok(h), Ok(m)) = (s[1..3].parse::(), s[3..5].parse::()) { - return sign * (h * 3600 + m * 60); - } - } - } - } - 0 -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn debounce_should_fire_after_window_elapses() { - use std::time::{Duration, Instant}; - let last = Instant::now(); - let debounce = Duration::from_millis(500); - assert!(!debounce_should_fire(last, last, debounce)); - assert!(!debounce_should_fire( - last, - last + Duration::from_millis(499), - debounce - )); - assert!(debounce_should_fire( - last, - last + Duration::from_millis(500), - debounce - )); - assert!(debounce_should_fire( - last, - last + Duration::from_secs(1), - debounce - )); - } - - #[test] - fn debounce_handles_now_before_last_event() { - use std::time::{Duration, Instant}; - let now = Instant::now(); - let later = now + Duration::from_secs(1); - assert!(!debounce_should_fire( - later, - now, - Duration::from_millis(500) - )); - } - - #[test] - fn debounce_collapses_burst_to_single_fire() { - use std::time::{Duration, Instant}; - let debounce = Duration::from_millis(500); - let t0 = Instant::now(); - let burst = [ - t0, - t0 + Duration::from_millis(50), - t0 + Duration::from_millis(120), - t0 + Duration::from_millis(200), - ]; - let last = *burst.last().unwrap(); - for &t in &burst { - assert!( - !debounce_should_fire(last, t, debounce), - "burst event at {:?} after last must not fire", - t.duration_since(t0) - ); - } - assert!(debounce_should_fire( - last, - last + Duration::from_millis(500), - debounce - )); - } -} diff --git a/crates/weaveffi-cli/src/config.rs b/crates/weaveffi-cli/src/config.rs index b03f42db..79fe51e9 100644 --- a/crates/weaveffi-cli/src/config.rs +++ b/crates/weaveffi-cli/src/config.rs @@ -1,11 +1,35 @@ -//! CLI configuration: the `cli_targets!` registry, the per-target -//! [`CliConfig`] struct it generates, [`GlobalConfig`], and the inline-IDL -//! `generators:` merge logic. - +//! Project configuration: `weaveffi.toml` discovery and parsing, the +//! `cli_targets!` registry, and the per-target [`Targets`] struct it +//! generates. +//! +//! A project's identity and generator options live in one TOML file next to +//! the IDL: +//! +//! ```toml +//! [package] +//! name = "kvstore" +//! version = "1.2.0" +//! license = "MIT" +//! +//! [global] +//! c_prefix = "kv" +//! +//! [generators.swift] +//! module_name = "KVStore" +//! +//! [generators.android] +//! package = "com.example.kvstore" +//! ``` +//! +//! The IDL itself describes only the API surface. `weaveffi generate` finds +//! the config automatically by walking up from the input file's directory +//! (like Cargo finds `Cargo.toml`); `--config ` names one explicitly. + +use camino::{Utf8Path, Utf8PathBuf}; use miette::{IntoDiagnostic, Result, WrapErr}; use serde::Deserialize; -use std::collections::BTreeMap; -use weaveffi_core::codegen::{ConfiguredGenerator, DynGenerator, OrchestratorHooks}; +use weaveffi_core::codegen::{ConfiguredBackend, OrchestratorHooks, Target}; +use weaveffi_core::pkg::Package; use weaveffi_gen_android::{AndroidConfig, AndroidGenerator}; use weaveffi_gen_c::{CConfig, CGenerator}; use weaveffi_gen_cpp::{CppConfig, CppGenerator}; @@ -18,15 +42,18 @@ use weaveffi_gen_ruby::{RubyConfig, RubyGenerator}; use weaveffi_gen_swift::{SwiftConfig, SwiftGenerator}; use weaveffi_gen_wasm::{WasmConfig, WasmGenerator}; +/// The file name `weaveffi generate` looks for when `--config` is absent. +pub(crate) 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 `CliConfig` struct: one typed field per target; -/// * `build_generators`: the ordered generator list; -/// * `apply_inline_target`: inline-IDL `generators.` dispatch; -/// * `stamp_input_basename` / `fan_strip_module_prefix`: the `finalize` -/// fan-outs. +/// * the [`Targets`] struct: one typed field per target, deserialized from +/// the `[generators.]` tables; +/// * `build`: the ordered, object-safe target list; +/// * `stamp_input_basename` / `fan_strip_module_prefix` / `fan_c_prefix`: +/// the `finalize` fan-outs. /// /// 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 @@ -39,46 +66,29 @@ macro_rules! cli_targets { ( $( $key:literal => $field:ident : $cfg:ident via $gen:ident $(, $strip:ident)? ),* $(,)? ) => { - /// Top-level configuration loaded from `--config `. Each - /// per-language field is the generator crate's own `Config` type, so - /// adding a knob to a generator only requires changing that - /// generator's crate. Inline `api.generators` overrides from the IDL - /// are merged on top after loading. Generated by `cli_targets!`. - #[derive(Debug, Default, Deserialize)] + /// 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)] - pub(crate) struct CliConfig { + pub(crate) struct Targets { $( pub(crate) $field: $cfg, )* - /// Global knobs that apply across all generators. The TOML file - /// may use `[global]` or `[weaveffi]` as the section name. - #[serde(alias = "weaveffi")] - pub(crate) global: GlobalConfig, } - impl CliConfig { - /// Build the heterogeneous list of object-safe generators that the + impl Targets { + /// Every registered target name, in registry order. + pub(crate) 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 test output. - pub(crate) fn build_generators(&self) -> Vec> { + /// order in the `cli_targets!` invocation for stable output. + pub(crate) fn build(&self) -> Vec> { vec![ - $( Box::new(ConfiguredGenerator::new($gen, self.$field.clone())), )* + $( Box::new(ConfiguredBackend::new($gen, self.$field.clone())), )* ] } - /// Deserialize one inline `generators.` table into the - /// matching per-target config, replacing it. Returns `false` for - /// unknown target names so the caller can ignore them. - fn apply_inline_target(&mut self, target: &str, table: toml::Value) -> bool { - match target { - $( $key => { - if let Ok(cfg) = <$cfg>::deserialize(table) { - self.$field = cfg; - } - true - } )* - _ => false, - } - } - /// Stamp the IDL basename onto every per-target config. fn stamp_input_basename(&mut self, input_basename: Option) { $( self.$field.input_basename = input_basename.clone(); )* @@ -86,9 +96,6 @@ macro_rules! cli_targets { /// Fan `global.strip_module_prefix` out to every target whose /// config honors it (those tagged `strip` in `cli_targets!`). - /// Stripping is the default, so the useful direction is `false`: - /// one `[global]` line restores module-prefixed wrapper names - /// across every supporting target. fn fan_strip_module_prefix(&mut self, value: bool) { $( $( cli_targets!(@strip self, $field, value, $strip); )? )* } @@ -96,8 +103,8 @@ macro_rules! cli_targets { /// Fan the resolved global C ABI `prefix` out to every per-target /// config that has not set its own. The C symbol prefix is global /// by nature: every consumer must call the identical exported - /// symbols, so a single `[global] c_prefix` (or `[c] prefix`) - /// reaches all eleven languages. + /// symbols, so a single `[global] c_prefix` reaches all eleven + /// languages. fn fan_c_prefix(&mut self, resolved: Option) { let Some(p) = resolved else { return }; $( @@ -127,9 +134,10 @@ cli_targets! { "ruby" => ruby: RubyConfig via RubyGenerator, strip, } -/// Knobs that affect multiple generators or the orchestrator itself. +/// Knobs that affect multiple generators or the orchestrator itself +/// (the `[global]` table). #[derive(Debug, Default, Clone, Deserialize)] -#[serde(default)] +#[serde(default, deny_unknown_fields)] pub(crate) struct GlobalConfig { /// Shorthand: sets `strip_module_prefix` on every per-target config that /// supports it. Stripping is the default, so `strip_module_prefix = @@ -147,44 +155,93 @@ pub(crate) struct GlobalConfig { pub(crate) post_generate: Option, } -impl CliConfig { - pub(crate) fn load(path: Option<&str>) -> Result { +/// The whole `weaveffi.toml`: package identity, global knobs, and per-target +/// generator options. +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub(crate) struct ProjectConfig { + /// The `[package]` table: distribution identity shared by every + /// generated manifest. + pub(crate) package: Package, + /// The `[global]` table. + pub(crate) global: GlobalConfig, + /// The `[generators.]` tables. + pub(crate) generators: Targets, + /// Where the config was loaded from, when it came from a file. + #[serde(skip)] + pub(crate) source: Option, +} + +impl ProjectConfig { + /// Load the project config: the file named by `explicit` when given, + /// otherwise the nearest `weaveffi.toml` at or above the input file's + /// directory, otherwise the defaults. + /// + /// # Errors + /// + /// Returns an error when an explicitly named file cannot be read, or when + /// any located file fails to parse. + pub(crate) fn load(explicit: Option<&str>, input: &Utf8Path) -> Result { + let path = match explicit { + Some(p) => Some(Utf8PathBuf::from(p)), + None => discover(input), + }; match path { - Some(p) => { - let contents = std::fs::read_to_string(p) - .into_diagnostic() - .wrap_err_with(|| format!("failed to read config file: {}", p))?; - toml::from_str(&contents) - .into_diagnostic() - .wrap_err_with(|| format!("failed to parse config file: {}", p)) - } + Some(p) => Self::from_file(&p), None => Ok(Self::default()), } } + /// Parse the config file at `path`. + /// + /// # Errors + /// + /// Returns an error when the file cannot be read or is not a valid + /// `weaveffi.toml`. + pub(crate) fn from_file(path: &Utf8Path) -> Result { + let contents = std::fs::read_to_string(path.as_std_path()) + .into_diagnostic() + .wrap_err_with(|| format!("failed to read config file: {path}"))?; + let mut cfg: Self = toml::from_str(&contents) + .into_diagnostic() + .wrap_err_with(|| format!("failed to parse config file: {path}"))?; + cfg.source = Some(path.to_path_buf()); + Ok(cfg) + } + /// Fan `global.strip_module_prefix` out to every per-target config that - /// honors it, propagate the C symbol prefix to the C++ wrapper config - /// when it was only set on `[c]`, then stamp the IDL `input_basename` - /// on every per-target config. Must run after [`merge_inline_generators`] - /// so per-target overrides can disable a flag or override the propagated - /// prefix. + /// honors it, propagate the C symbol prefix to every target that did not + /// set its own, then stamp the IDL `input_basename` on every per-target + /// config. pub(crate) fn finalize(&mut self, input_basename: Option) { if let Some(v) = self.global.strip_module_prefix { - self.fan_strip_module_prefix(v); + self.generators.fan_strip_module_prefix(v); } // The C ABI prefix is global: every backend must call the exact same - // `extern "C"` symbols. Resolve it once (`[global] c_prefix` wins, then - // `[c] prefix`) and fan it out to every per-target config so a custom - // prefix is honored across all eleven languages, not just C/C++. + // `extern "C"` symbols. Resolve it once (`[global] c_prefix` wins, + // then `[generators.c] prefix`) and fan it out to every per-target + // config so a custom prefix is honored across all eleven languages. let resolved_prefix = self .global .c_prefix .clone() - .or_else(|| self.c.prefix.clone()); - self.fan_c_prefix(resolved_prefix); - self.stamp_input_basename(input_basename); + .or_else(|| self.generators.c.prefix.clone()); + self.generators.fan_c_prefix(resolved_prefix); + self.generators.stamp_input_basename(input_basename); + } + + /// Load, then finalize against the input path in one step. + /// + /// # Errors + /// + /// Propagates errors from [`ProjectConfig::load`]. + pub(crate) fn for_input(explicit: Option<&str>, input: &Utf8Path) -> Result { + let mut cfg = Self::load(explicit, input)?; + cfg.finalize(input.file_name().map(str::to_string)); + Ok(cfg) } + /// The orchestrator hooks configured in `[global]`. pub(crate) fn hooks(&self) -> OrchestratorHooks { OrchestratorHooks { pre_generate: self.global.pre_generate.clone(), @@ -192,297 +249,172 @@ impl CliConfig { } } - /// The C ABI prefix passed to the Rust scaffold generator. Mirrors the - /// C generator's `prefix` so the scaffold's `#[no_mangle]` symbols line - /// up with what the C/C++ wrappers call. - pub(crate) fn scaffold_prefix(&self) -> String { - self.c.prefix().to_string() + /// Build every registered target, keeping only those named in the + /// comma-separated `filter` (all of them when `filter` is `None`). + /// + /// # 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(crate) fn select_targets(&self, filter: Option<&str>) -> Result>> { + let wanted: Option> = filter.map(|t| { + t.split(',') + .map(str::trim) + .filter(|s| !s.is_empty()) + .collect() + }); + if let Some(names) = &wanted { + let unknown: Vec<&str> = names + .iter() + .copied() + .filter(|n| !Targets::NAMES.contains(n)) + .collect(); + if !unknown.is_empty() { + miette::bail!( + "unknown target(s): {}; expected any of: {}", + unknown.join(", "), + Targets::NAMES.join(", ") + ); + } + } + Ok(self + .generators + .build() + .into_iter() + .filter(|g| wanted.as_ref().is_none_or(|ts| ts.contains(&g.name()))) + .collect()) } } -/// Apply per-target overrides from the IDL's `generators:` block to the -/// loaded [`CliConfig`]. -/// -/// Inline IDL values **override** anything supplied via `--config `, -/// because the IDL is project-local and committed alongside the API -/// definition while a TOML file is typically per-environment. -/// -/// For each per-target section (`swift`, `c`, `cpp`, …) the body is -/// deserialized directly into that target's `Config` struct, overwriting any -/// fields the user set. The special `weaveffi`/`global` section maps to -/// [`GlobalConfig`]. -/// -/// Unknown target names and unknown keys are silently ignored so that older -/// CLIs can read newer IDL files without crashing. -pub(crate) fn merge_inline_generators( - config: &mut CliConfig, - generators: &BTreeMap, -) { - for (target, value) in generators { - let Some(table) = value.as_table() else { - continue; - }; - let table_value = toml::Value::Table(table.clone()); - - match target.as_str() { - "global" | "weaveffi" => { - if let Ok(g) = GlobalConfig::deserialize(table_value) { - config.global = g; - } - } - // Per-target sections dispatch through the `cli_targets!`-generated - // table; unknown names return `false` and are silently ignored so - // older CLIs can read newer IDL files. - other => { - config.apply_inline_target(other, table_value); - } +/// Walk from the input file's directory up to the filesystem root and return +/// the first `weaveffi.toml` found. +fn discover(input: &Utf8Path) -> Option { + let start = input + .parent() + .filter(|p| !p.as_str().is_empty()) + .map(Utf8Path::to_path_buf) + .unwrap_or_else(|| Utf8PathBuf::from(".")); + let mut dir = Some(start.as_path()); + while let Some(d) = dir { + let candidate = d.join(CONFIG_FILE_NAME); + if candidate.is_file() { + return Some(candidate); } + dir = d.parent().filter(|p| !p.as_str().is_empty()); } + None } #[cfg(test)] mod tests { use super::*; + fn write(dir: &std::path::Path, name: &str, contents: &str) -> Utf8PathBuf { + let p = dir.join(name); + std::fs::write(&p, contents).unwrap(); + Utf8PathBuf::from_path_buf(p).unwrap() + } + #[test] - fn config_file_parsed() { + fn parses_package_global_and_generator_tables() { let dir = tempfile::tempdir().unwrap(); - let cfg_path = dir.path().join("weaveffi.toml"); - std::fs::write( - &cfg_path, + let cfg_path = write( + dir.path(), + CONFIG_FILE_NAME, concat!( - "[swift]\n", - "module_name = \"MyApp\"\n", - "[android]\n", - "package = \"com.example.myapp\"\n", + "[package]\n", + "name = \"kvstore\"\n", + "version = \"1.2.0\"\n", "[global]\n", "strip_module_prefix = true\n", + "c_prefix = \"kv\"\n", + "[generators.swift]\n", + "module_name = \"MyApp\"\n", + "[generators.android]\n", + "package = \"com.example.myapp\"\n", ), - ) - .unwrap(); - - let cfg = CliConfig::load(Some(cfg_path.to_str().unwrap())).unwrap(); - assert_eq!(cfg.swift.module_name(), "MyApp"); - assert_eq!(cfg.android.package(), "com.example.myapp"); - assert_eq!(cfg.global.strip_module_prefix, Some(true)); - assert_eq!(cfg.c.prefix(), "weaveffi"); - } - - #[test] - fn inline_generator_config() { - let yaml = concat!( - "version: \"0.7.0\"\n", - "modules:\n", - " - name: math\n", - " functions:\n", - " - name: add\n", - " params:\n", - " - { name: a, type: i32 }\n", - " - { name: b, type: i32 }\n", - " return: i32\n", - "generators:\n", - " swift:\n", - " module_name: MySwiftModule\n", - ); - let api: weaveffi_ir::ir::Api = serde_yaml::from_str(yaml).unwrap(); - assert!(api.generators.is_some()); - - let mut config = CliConfig::default(); - merge_inline_generators(&mut config, api.generators.as_ref().unwrap()); - assert_eq!(config.swift.module_name(), "MySwiftModule"); - } - - #[test] - fn inline_dart_package_name_merges() { - let yaml = concat!( - "version: \"0.7.0\"\n", - "modules:\n", - " - name: m\n", - " functions: []\n", - "generators:\n", - " dart:\n", - " package_name: my_dart_pkg\n", ); - let api: weaveffi_ir::ir::Api = serde_yaml::from_str(yaml).unwrap(); - let mut config = CliConfig::default(); - merge_inline_generators(&mut config, api.generators.as_ref().unwrap()); - assert_eq!(config.dart.package_name(), "my_dart_pkg"); - } + let mut cfg = ProjectConfig::from_file(&cfg_path).unwrap(); + assert_eq!(cfg.package.name.as_deref(), Some("kvstore")); + assert_eq!(cfg.package.version.as_deref(), Some("1.2.0")); + assert_eq!(cfg.generators.swift.module_name(), "MyApp"); + assert_eq!(cfg.generators.android.package(), "com.example.myapp"); + assert_eq!(cfg.global.strip_module_prefix, Some(true)); + assert_eq!(cfg.source.as_deref(), Some(cfg_path.as_path())); - #[test] - fn inline_go_module_path_merges() { - let yaml = concat!( - "version: \"0.7.0\"\n", - "modules:\n", - " - name: m\n", - " functions: []\n", - "generators:\n", - " go:\n", - " module_path: github.com/me/myffi\n", + cfg.finalize(Some("api.yml".into())); + assert_eq!(cfg.generators.c.prefix(), "kv"); + assert_eq!(cfg.generators.python.prefix(), "kv"); + assert!(cfg.generators.python.strip_module_prefix); + assert_eq!( + cfg.generators.swift.input_basename.as_deref(), + Some("api.yml") ); - let api: weaveffi_ir::ir::Api = serde_yaml::from_str(yaml).unwrap(); - let mut config = CliConfig::default(); - merge_inline_generators(&mut config, api.generators.as_ref().unwrap()); - assert_eq!(config.go.module_path(), "github.com/me/myffi"); } #[test] - fn inline_ruby_module_name_merges() { - let yaml = concat!( - "version: \"0.7.0\"\n", - "modules:\n", - " - name: m\n", - " functions: []\n", - "generators:\n", - " ruby:\n", - " module_name: MyRubyMod\n", + fn unknown_top_level_keys_are_errors() { + let dir = tempfile::tempdir().unwrap(); + let cfg_path = write( + dir.path(), + CONFIG_FILE_NAME, + "[swift]\nmodule_name = \"X\"\n", ); - let api: weaveffi_ir::ir::Api = serde_yaml::from_str(yaml).unwrap(); - let mut config = CliConfig::default(); - merge_inline_generators(&mut config, api.generators.as_ref().unwrap()); - assert_eq!(config.ruby.module_name(), "MyRubyMod"); + let err = ProjectConfig::from_file(&cfg_path).unwrap_err(); + assert!(format!("{err:?}").contains("swift"), "{err:?}"); } #[test] - fn inline_ruby_gem_name_merges() { - let yaml = concat!( - "version: \"0.7.0\"\n", - "modules:\n", - " - name: m\n", - " functions: []\n", - "generators:\n", - " ruby:\n", - " gem_name: my_ruby_gem\n", + fn discovery_walks_up_from_the_input() { + let dir = tempfile::tempdir().unwrap(); + let nested = dir.path().join("idl").join("deep"); + std::fs::create_dir_all(&nested).unwrap(); + write( + dir.path(), + CONFIG_FILE_NAME, + "[package]\nname = \"found\"\n", ); - let api: weaveffi_ir::ir::Api = serde_yaml::from_str(yaml).unwrap(); - let mut config = CliConfig::default(); - merge_inline_generators(&mut config, api.generators.as_ref().unwrap()); - assert_eq!(config.ruby.gem_name(), "my_ruby_gem"); - } + let input = write(&nested, "api.yml", "version: \"0.8.0\"\nmodules: []\n"); - #[test] - fn inline_global_strip_module_prefix_merges() { - let yaml = concat!( - "version: \"0.7.0\"\n", - "modules:\n", - " - name: m\n", - " functions: []\n", - "generators:\n", - " weaveffi:\n", - " strip_module_prefix: true\n", - ); - let api: weaveffi_ir::ir::Api = serde_yaml::from_str(yaml).unwrap(); - let mut config = CliConfig::default(); - assert_eq!(config.global.strip_module_prefix, None); - merge_inline_generators(&mut config, api.generators.as_ref().unwrap()); - config.finalize(None); - assert_eq!(config.global.strip_module_prefix, Some(true)); - assert!( - config.python.strip_module_prefix, - "finalize should fan strip_module_prefix out to python" - ); - assert!( - config.swift.strip_module_prefix, - "finalize should fan strip_module_prefix out to swift" - ); + let cfg = ProjectConfig::load(None, &input).unwrap(); + assert_eq!(cfg.package.name.as_deref(), Some("found")); } #[test] - fn inline_global_pre_generate_merges() { - let yaml = concat!( - "version: \"0.7.0\"\n", - "modules:\n", - " - name: m\n", - " functions: []\n", - "generators:\n", - " global:\n", - " pre_generate: \"echo hi\"\n", - ); - let api: weaveffi_ir::ir::Api = serde_yaml::from_str(yaml).unwrap(); - let mut config = CliConfig::default(); - merge_inline_generators(&mut config, api.generators.as_ref().unwrap()); - assert_eq!(config.global.pre_generate.as_deref(), Some("echo hi")); - } - - #[test] - fn inline_unknown_target_silently_ignored() { - let yaml = concat!( - "version: \"0.7.0\"\n", - "modules:\n", - " - name: m\n", - " functions: []\n", - "generators:\n", - " rustlang:\n", - " crate_name: future_target\n", - " swift:\n", - " module_name: KeptSwift\n", - ); - let api: weaveffi_ir::ir::Api = serde_yaml::from_str(yaml).unwrap(); - let mut config = CliConfig::default(); - merge_inline_generators(&mut config, api.generators.as_ref().unwrap()); - assert_eq!(config.swift.module_name(), "KeptSwift"); + fn defaults_when_no_config_exists() { + let dir = tempfile::tempdir().unwrap(); + let input = write(dir.path(), "api.yml", "version: \"0.8.0\"\nmodules: []\n"); + let cfg = ProjectConfig::load(None, &input).unwrap(); + assert!(cfg.source.is_none(), "unexpected config: {:?}", cfg.source); + assert_eq!(cfg.generators.swift.module_name(), "WeaveFFI"); + assert_eq!(cfg.generators.android.package(), "com.weaveffi"); + assert_eq!(cfg.global.strip_module_prefix, None); + assert!(cfg.package.name.is_none()); } #[test] - fn inline_unknown_key_silently_ignored() { - let yaml = concat!( - "version: \"0.7.0\"\n", - "modules:\n", - " - name: m\n", - " functions: []\n", - "generators:\n", - " swift:\n", - " module_name: KeptSwift\n", - " unrecognized_future_key: \"some value\"\n", - ); - let api: weaveffi_ir::ir::Api = serde_yaml::from_str(yaml).unwrap(); - let mut config = CliConfig::default(); - merge_inline_generators(&mut config, api.generators.as_ref().unwrap()); - assert_eq!(config.swift.module_name(), "KeptSwift"); + fn explicit_missing_config_is_an_error() { + let err = ProjectConfig::load(Some("/nonexistent/weaveffi.toml"), Utf8Path::new("x.yml")) + .unwrap_err(); + assert!(format!("{err:?}").contains("failed to read config file")); } #[test] - fn inline_generator_config_overrides_file() { - let yaml = concat!( - "version: \"0.7.0\"\n", - "modules:\n", - " - name: math\n", - " functions:\n", - " - name: add\n", - " params:\n", - " - { name: a, type: i32 }\n", - " - { name: b, type: i32 }\n", - " return: i32\n", - "generators:\n", - " swift:\n", - " module_name: FromIDL\n", - " android:\n", - " package: com.idl.app\n", - ); - let api: weaveffi_ir::ir::Api = serde_yaml::from_str(yaml).unwrap(); - - let mut config = CliConfig { - swift: SwiftConfig { - module_name: Some("FromFile".into()), - ..SwiftConfig::default() - }, - android: AndroidConfig { - package: Some("com.file.app".into()), - ..AndroidConfig::default() - }, - ..CliConfig::default() + fn target_filter_rejects_unknown_names() { + let cfg = ProjectConfig::default(); + let err = match cfg.select_targets(Some("swift,rustlang")) { + Ok(_) => panic!("unknown target should be rejected"), + Err(e) => e, }; - merge_inline_generators(&mut config, api.generators.as_ref().unwrap()); - assert_eq!(config.swift.module_name(), "FromIDL"); - assert_eq!(config.android.package(), "com.idl.app"); - } - - #[test] - fn config_default_when_no_file() { - let cfg = CliConfig::load(None).unwrap(); - assert_eq!(cfg.swift.module_name(), "WeaveFFI"); - assert_eq!(cfg.android.package(), "com.weaveffi"); - assert_eq!(cfg.global.strip_module_prefix, None); + assert!(format!("{err}").contains("rustlang"), "{err}"); + let ok = cfg.select_targets(Some("swift, 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() + ); } /// Every registered target's *declared* [`TargetCapabilities`] must match @@ -494,8 +426,7 @@ mod tests { /// Declaration accuracy is only half the contract. The companion guard in /// `tests/no_silent_stubs.rs` proves a *declared* feature is actually /// emitted (no `unimplemented!`/`todo!` source, no stub markers in output), - /// and the conformance harness proves it runs. Together they make "declared - /// capabilities match reality" enforceable rather than aspirational. + /// and the conformance harness proves it runs. #[test] fn declared_capabilities_match_documented_matrix() { use std::collections::BTreeMap; @@ -504,43 +435,25 @@ mod tests { // Every target is full. Wasm delivers callbacks/listeners // synchronously through function-table trampolines in its standard // loader (Emscripten mode stubs them, mirroring its async stubs). - let expected: &[(&str, TargetCapabilities)] = &[ - ("c", TargetCapabilities::full()), - ("cpp", TargetCapabilities::full()), - ("swift", TargetCapabilities::full()), - ("android", TargetCapabilities::full()), - ("node", TargetCapabilities::full()), - ("wasm", TargetCapabilities::full()), - ("python", TargetCapabilities::full()), - ("dotnet", TargetCapabilities::full()), - ("dart", TargetCapabilities::full()), - ("go", TargetCapabilities::full()), - ("ruby", TargetCapabilities::full()), - ]; - - let generators = CliConfig::default().build_generators(); - let actual: BTreeMap<&str, TargetCapabilities> = generators + let expected: Vec<(&str, TargetCapabilities)> = Targets::NAMES + .iter() + .map(|n| (*n, TargetCapabilities::full())) + .collect(); + + let targets = Targets::default().build(); + let actual: BTreeMap<&str, TargetCapabilities> = targets .iter() .map(|g| (g.name(), g.capabilities())) .collect(); - assert_eq!( - actual.len(), - expected.len(), - "registry has {} targets but the matrix lists {}; add the new \ - target to the matrix (and the docs feature table)", - actual.len(), - expected.len(), - ); + assert_eq!(actual.len(), expected.len()); for (name, caps) in expected { - let got = actual.get(name).unwrap_or_else(|| { - panic!("target '{name}' is missing from the generator registry") - }); + let got = actual + .get(name) + .unwrap_or_else(|| panic!("target '{name}' is missing from the registry")); assert_eq!( - got, caps, - "declared capabilities for '{name}' drifted from the documented \ - matrix; reconcile the generator's capabilities(), this table, and \ - docs/src/generators/README.md", + *got, caps, + "declared capabilities for '{name}' drifted from the documented matrix", ); } } diff --git a/crates/weaveffi-cli/src/doctor.rs b/crates/weaveffi-cli/src/doctor.rs deleted file mode 100644 index a9db27fc..00000000 --- a/crates/weaveffi-cli/src/doctor.rs +++ /dev/null @@ -1,528 +0,0 @@ -//! `weaveffi doctor`: probe the host for the toolchains each language target -//! needs (compilers, SDKs, Rust cross-targets, …) and report what is present. -//! -//! Each probe yields a [`DoctorCheck`]; `--format json` serializes the raw -//! list, otherwise the checks are grouped into human-readable sections. With a -//! `--target` filter the command exits non-zero if any *relevant* check fails, -//! so it can gate a language's build in CI. - -use miette::{IntoDiagnostic, Result, WrapErr}; -use serde::Serialize; -use std::collections::BTreeSet; -use std::env; -use std::process::Command; -use weaveffi_core::platform::Platform; - -/// A single toolchain check produced by `cmd_doctor`. Serialized as JSON when -/// the user passes `--format json` and grouped into sections for the -/// human-readable output otherwise. `applies_to` is the set of target names -/// (`swift`, `android`, `cpp`, …) for which this check is relevant; the -/// special value `"*"` means the check is always run regardless of any -/// `--target` filter (e.g. the required Rust toolchain). -#[derive(Debug, Serialize)] -struct DoctorCheck { - id: String, - name: String, - ok: bool, - version: Option, - hint: Option, - applies_to: Vec<&'static str>, -} - -impl DoctorCheck { - fn applies_to_target(&self, target: Option<&str>) -> bool { - match target { - None => true, - Some(t) => self.applies_to.contains(&"*") || self.applies_to.contains(&t), - } - } -} - -pub(crate) fn cmd_doctor(target: Option<&str>, format: Option<&str>) -> Result<()> { - let checks: Vec = collect_doctor_checks() - .into_iter() - .filter(|c| c.applies_to_target(target)) - .collect(); - - if format == Some("json") { - let json = serde_json::to_string_pretty(&checks) - .into_diagnostic() - .wrap_err("failed to serialize doctor checks")?; - println!("{json}"); - } else { - print_doctor_human(&checks); - } - - if target.is_some() && checks.iter().any(|c| !c.ok) { - std::process::exit(1); - } - - Ok(()) -} - -fn collect_doctor_checks() -> Vec { - let mut checks = Vec::new(); - - checks.push(tool_check( - "rustc", - "rustc", - &["--version"], - "Rust compiler", - Some("Install via https://rustup.rs"), - vec!["*"], - )); - checks.push(tool_check( - "cargo", - "cargo", - &["--version"], - "Cargo (Rust package manager)", - Some("Install via https://rustup.rs"), - vec!["*"], - )); - - if cfg!(target_os = "macos") { - checks.push(tool_check( - "xcodebuild", - "xcodebuild", - &["-version"], - "Xcode command-line tools", - Some("Install Xcode from the App Store, then run `xcode-select --install`"), - vec!["swift"], - )); - } - - checks.push(ndk_check()); - - checks.push(tool_check( - "node", - "node", - &["-v"], - "Node.js", - Some("Install from https://nodejs.org or with your package manager"), - vec!["node"], - )); - checks.push(tool_check( - "npm", - "npm", - &["-v"], - "npm", - Some("Install Node.js which includes npm, or use pnpm/yarn"), - vec!["node"], - )); - - checks.extend(cross_target_checks()); - checks.extend(packaging_target_checks()); - - checks.push(tool_check( - "wasm-pack", - "wasm-pack", - &["--version"], - "wasm-pack", - Some("install with `cargo install wasm-pack`"), - vec!["wasm"], - )); - checks.push(tool_check( - "wasm-bindgen", - "wasm-bindgen", - &["--version"], - "wasm-bindgen-cli", - Some("install with `cargo install wasm-bindgen-cli`"), - vec!["wasm"], - )); - - checks.push(tool_check( - "cmake", - "cmake", - &["--version"], - "CMake", - Some("Install via https://cmake.org or your package manager"), - vec!["cpp"], - )); - checks.push(cxx_compiler_check()); - - checks.push(tool_check( - "dart", - "dart", - &["--version"], - "Dart SDK", - Some("Install from https://dart.dev/get-dart"), - vec!["dart"], - )); - checks.push(tool_check( - "flutter", - "flutter", - &["--version"], - "Flutter (optional)", - Some("Install from https://flutter.dev/docs/get-started/install"), - vec!["dart"], - )); - - checks.push(tool_check( - "go", - "go", - &["version"], - "Go", - Some("Install from https://go.dev/dl"), - vec!["go"], - )); - - checks.push(tool_check( - "ruby", - "ruby", - &["--version"], - "Ruby", - Some("Install from https://www.ruby-lang.org or use rbenv/rvm"), - vec!["ruby"], - )); - checks.push(tool_check( - "gem", - "gem", - &["--version"], - "RubyGems", - Some("Install Ruby which includes gem"), - vec!["ruby"], - )); - checks.push(tool_check( - "bundler", - "bundler", - &["--version"], - "Bundler", - Some("Install with `gem install bundler`"), - vec!["ruby"], - )); - - checks.push(tool_check( - "dotnet", - "dotnet", - &["--version"], - ".NET SDK", - Some("Install from https://dotnet.microsoft.com/download"), - vec!["dotnet"], - )); - - checks.push(tool_check( - "python3", - "python3", - &["--version"], - "Python 3", - Some("Install Python 3 from https://www.python.org or your package manager"), - vec!["python"], - )); - checks.push(python_ctypes_check()); - - checks -} - -fn tool_check( - id: &str, - cmd: &str, - args: &[&str], - name: &str, - hint: Option<&str>, - applies_to: Vec<&'static str>, -) -> DoctorCheck { - let (ok, version) = match Command::new(cmd).args(args).output() { - Ok(out) if out.status.success() => { - let stdout = String::from_utf8_lossy(&out.stdout); - let stderr = String::from_utf8_lossy(&out.stderr); - let ver = stdout - .lines() - .next() - .map(str::trim) - .filter(|s| !s.is_empty()) - .or_else(|| { - stderr - .lines() - .next() - .map(str::trim) - .filter(|s| !s.is_empty()) - }) - .map(|s| s.to_string()); - (true, ver) - } - _ => (false, None), - }; - DoctorCheck { - id: id.to_string(), - name: name.to_string(), - ok, - version, - hint: hint.map(str::to_string), - applies_to, - } -} - -fn ndk_check() -> DoctorCheck { - let result = Command::new("ndk-build").arg("-v").output(); - let ok = matches!(&result, Ok(o) if o.status.success()); - let version = if ok { - result.ok().and_then(|out| { - String::from_utf8_lossy(&out.stdout) - .lines() - .next() - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) - }) - } else { - None - }; - let hint = if ok { - None - } else { - let env_ok = env::var_os("ANDROID_NDK_HOME") - .map(|p| std::path::Path::new(&p).exists()) - .unwrap_or(false) - || env::var_os("ANDROID_NDK_ROOT") - .map(|p| std::path::Path::new(&p).exists()) - .unwrap_or(false); - if env_ok { - Some("ANDROID_NDK_HOME/ROOT is set; ensure `ndk-build` is in PATH".to_string()) - } else if cfg!(target_os = "macos") { - Some( - "Install via Android Studio SDK Manager or `brew install android-ndk`. Set ANDROID_NDK_HOME." - .to_string(), - ) - } else { - Some("Install via Android Studio SDK Manager. Set ANDROID_NDK_HOME.".to_string()) - } - }; - DoctorCheck { - id: "ndk-build".to_string(), - name: "Android NDK (ndk-build)".to_string(), - ok, - version, - hint, - applies_to: vec!["android"], - } -} - -/// The raw output of `rustup target list --installed`, or `None` when rustup -/// is unavailable. Shared by the cross-compilation and packaging probes so the -/// command shells out to rustup at most once. -fn installed_rust_targets() -> Option { - Command::new("rustup") - .args(["target", "list", "--installed"]) - .output() - .ok() - .and_then(|out| { - out.status - .success() - .then(|| String::from_utf8_lossy(&out.stdout).into_owned()) - }) -} - -/// True when `target` appears in an installed-target listing produced by -/// [`installed_rust_targets`]. -fn has_rust_target(installed: Option<&String>, target: &str) -> bool { - installed - .map(|s| s.lines().any(|line| line.trim() == target)) - .unwrap_or(false) -} - -fn cross_target_checks() -> Vec { - let installed = installed_rust_targets(); - - let targets: [(&str, &str, &str, Vec<&'static str>); 3] = [ - ( - "target_ios", - "aarch64-apple-ios", - "iOS target (aarch64-apple-ios)", - vec!["swift"], - ), - ( - "target_android", - "aarch64-linux-android", - "Android target (aarch64-linux-android)", - vec!["android"], - ), - ( - "target_wasm", - "wasm32-unknown-unknown", - "WebAssembly target (wasm32-unknown-unknown)", - vec!["wasm"], - ), - ]; - - targets - .into_iter() - .map(|(id, target, name, applies_to)| { - let ok = has_rust_target(installed.as_ref(), target); - let hint = if ok { - None - } else { - Some(format!("install with `rustup target add {target}`")) - }; - DoctorCheck { - id: id.to_string(), - name: name.to_string(), - ok, - version: None, - hint, - applies_to, - } - }) - .collect() -} - -/// Probe the Rust cross-targets `weaveffi package --build` needs to produce a -/// native library for every platform in the v1 packaging matrix. These checks -/// apply to the `package` pseudo-target so `weaveffi doctor --target package` -/// can gate a cross-build in CI; only the host platform's target is needed when -/// building locally for a single platform. -fn packaging_target_checks() -> Vec { - let installed = installed_rust_targets(); - Platform::ALL - .into_iter() - .map(|platform| { - let target = platform.rust_target(); - let ok = has_rust_target(installed.as_ref(), target); - DoctorCheck { - id: format!("pkg_target_{}", platform.id().replace('-', "_")), - name: format!("{} producer target ({target})", platform.display_name()), - ok, - version: None, - hint: if ok { - None - } else { - Some(format!( - "needed only for `weaveffi package --build` targeting {}; \ - install with `rustup target add {target}`", - platform.id() - )) - }, - applies_to: vec!["package"], - } - }) - .collect() -} - -fn cxx_compiler_check() -> DoctorCheck { - for cmd in ["g++", "clang++"] { - if let Ok(out) = Command::new(cmd).arg("--version").output() { - if out.status.success() { - let ver = String::from_utf8_lossy(&out.stdout) - .lines() - .next() - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()); - return DoctorCheck { - id: "cxx_compiler".to_string(), - name: format!("C++ compiler ({cmd})"), - ok: true, - version: ver, - hint: None, - applies_to: vec!["cpp"], - }; - } - } - } - DoctorCheck { - id: "cxx_compiler".to_string(), - name: "C++ compiler (g++ or clang++)".to_string(), - ok: false, - version: None, - hint: Some("Install g++ or clang++ via your package manager".to_string()), - applies_to: vec!["cpp"], - } -} - -fn python_ctypes_check() -> DoctorCheck { - let ok = Command::new("python3") - .args(["-c", "import ctypes"]) - .output() - .map(|out| out.status.success()) - .unwrap_or(false); - DoctorCheck { - id: "python_ctypes".to_string(), - name: "Python ctypes module".to_string(), - ok, - version: None, - hint: if ok { - None - } else { - Some( - "ctypes ships with the Python 3 standard library; reinstall Python 3 (with libffi) and re-run `python3 -c 'import ctypes'`" - .to_string(), - ) - }, - applies_to: vec!["python"], - } -} - -fn print_doctor_human(checks: &[DoctorCheck]) { - println!("WeaveFFI Doctor: checking toolchain prerequisites\n"); - - let sections: &[(&str, &[&str])] = &[ - ("Required toolchain", &["rustc", "cargo"]), - ("Swift / iOS", &["xcodebuild"]), - ("Android", &["ndk-build"]), - ("Node.js", &["node", "npm"]), - ( - "Cross-compilation targets", - &["target_ios", "target_android", "target_wasm"], - ), - ( - "Packaging producer targets (--build)", - &[ - "pkg_target_darwin_arm64", - "pkg_target_darwin_x64", - "pkg_target_linux_x64", - "pkg_target_linux_arm64", - "pkg_target_windows_x64", - ], - ), - ("WebAssembly tools", &["wasm-pack", "wasm-bindgen"]), - ("C++", &["cmake", "cxx_compiler"]), - ("Dart", &["dart", "flutter"]), - ("Go", &["go"]), - ("Ruby", &["ruby", "gem", "bundler"]), - (".NET", &["dotnet"]), - ("Python", &["python3", "python_ctypes"]), - ]; - - let mut printed: BTreeSet<&str> = BTreeSet::new(); - for (heading, ids) in sections { - let in_section: Vec<&DoctorCheck> = checks - .iter() - .filter(|c| ids.contains(&c.id.as_str())) - .collect(); - if in_section.is_empty() { - continue; - } - println!("{heading}:"); - for c in in_section { - print_check_line(c); - printed.insert(c.id.as_str()); - } - println!(); - } - - let leftovers: Vec<&DoctorCheck> = checks - .iter() - .filter(|c| !printed.contains(c.id.as_str())) - .collect(); - if !leftovers.is_empty() { - println!("Other:"); - for c in leftovers { - print_check_line(c); - } - println!(); - } - - println!("Doctor completed. Address any missing items above."); -} - -fn print_check_line(c: &DoctorCheck) { - let status = if c.ok { "OK" } else { "MISSING" }; - match &c.version { - Some(ver) => println!("- {}: {} ({})", c.name, status, ver), - None => println!("- {}: {}", c.name, status), - } - if !c.ok { - if let Some(h) = &c.hint { - println!(" hint: {h}"); - } - } -} diff --git a/crates/weaveffi-cli/src/extract.rs b/crates/weaveffi-cli/src/extract.rs index 70662289..af24a941 100644 --- a/crates/weaveffi-cli/src/extract.rs +++ b/crates/weaveffi-cli/src/extract.rs @@ -38,8 +38,8 @@ pub(crate) fn cmd_extract( .map_err(|e| miette!("failed to extract API from Rust source {input}:\n{e}"))?; // Serialize the resolved form when the API validates (so cross-module - // references are emitted qualified, matching `weaveffi format`); fall - // back to the raw extracted document under `--warn`. + // references are emitted qualified and stable across runs); fall back + // to the raw extracted document under `--warn`. let api = match weaveffi_core::validate::validate_api(api.clone(), None) { Ok(resolved) => resolved.api().clone(), Err(e) => { diff --git a/crates/weaveffi-cli/src/main.rs b/crates/weaveffi-cli/src/main.rs index a9e3d2bb..d8674237 100644 --- a/crates/weaveffi-cli/src/main.rs +++ b/crates/weaveffi-cli/src/main.rs @@ -1,16 +1,13 @@ //! `weaveffi` command-line entry point: `clap` definitions and dispatch. //! -//! Each subcommand's implementation lives in `commands` (or its own -//! top-level module for the self-contained ones: `doctor`, `extract`, -//! `scaffold`); the generator registry and config plumbing live in -//! `config`. +//! Each subcommand's implementation lives in `commands` (or `extract` for the +//! Rust-source extractor); project configuration and the generator registry +//! live in `config`. mod commands; mod config; -mod doctor; mod extract; mod report; -mod scaffold; use clap::{CommandFactory, Parser, Subcommand, ValueEnum}; use miette::{bail, IntoDiagnostic, Result, WrapErr}; @@ -45,11 +42,9 @@ impl HumanJsonFormat { #[derive(Subcommand, Debug)] enum Commands { - New { - name: String, - }, + /// Generate bindings for every (or the selected) language target Generate { - /// Input IDL/IR file (yaml|yml|json|toml) + /// Input: annotated Rust source (.rs) or an IDL document (yaml|yml|json|toml) input: String, /// Output directory for generated artifacts #[arg(short, long, default_value = "./generated")] @@ -57,10 +52,7 @@ enum Commands { /// Comma-separated list of targets to generate (c, cpp, swift, android, node, wasm, python, dotnet, dart, go, ruby) #[arg(short, long)] target: Option, - /// Also generate a scaffold.rs with Rust FFI function stubs - #[arg(long)] - scaffold: bool, - /// Path to a TOML configuration file for generator options + /// Path to weaveffi.toml (default: the nearest one at or above the input file) #[arg(long)] config: Option, /// Print non-fatal warnings after validation @@ -73,18 +65,20 @@ enum Commands { #[arg(long)] dry_run: bool, }, + /// Validate an API definition without generating anything Validate { - /// Input IDL/IR file (yaml|yml|json|toml) + /// Input: annotated Rust source (.rs) or an IDL document (yaml|yml|json|toml) input: String, - /// Print non-fatal warnings after validation + /// Also report non-fatal warnings (advisory lints) #[arg(long)] warn: bool, /// Output format: `json` for machine-readable output, otherwise human-readable #[arg(long)] format: Option, }, + /// Assemble publishable packages that bundle prebuilt native libraries Package { - /// Input IDL/IR file (yaml|yml|json|toml) + /// Input: annotated Rust source (.rs) or an IDL document (yaml|yml|json|toml) input: String, /// Output directory for the packaged artifacts #[arg(short, long, default_value = "./dist")] @@ -92,7 +86,7 @@ enum Commands { /// Comma-separated list of targets to package (e.g. node,python,dotnet) #[arg(short, long)] target: Option, - /// Path to a TOML configuration file for generator options + /// Path to weaveffi.toml (default: the nearest one at or above the input file) #[arg(long)] config: Option, /// Directory of prebuilt native libraries laid out as `//` @@ -109,6 +103,7 @@ enum Commands { #[arg(long)] warn: bool, }, + /// Extract an IDL document from annotated Rust source Extract { /// Path to a Rust source file to extract API definitions from input: String, @@ -124,63 +119,30 @@ enum Commands { #[arg(long)] warn: bool, }, - Lint { - /// Input IDL/IR file (yaml|yml|json|toml) - input: String, - /// Output format: `json` for machine-readable output, otherwise human-readable - #[arg(long)] - format: Option, - }, + /// Show how regenerating would change an existing output directory Diff { - /// Input IDL/IR file (yaml|yml|json|toml) + /// Input: annotated Rust source (.rs) or an IDL document (yaml|yml|json|toml) input: String, /// Output directory to compare against (defaults to ./generated) #[arg(short, long)] out: Option, + /// Path to weaveffi.toml (default: the nearest one at or above the input file) + #[arg(long)] + config: Option, /// Exit non-zero if regeneration would change `out` (2 if files /// differ, 3 if files are missing/extra). Prints only a summary, /// not per-file diffs. #[arg(long)] check: bool, }, - Doctor { - /// Only run checks whose `applies_to` includes this target (e.g. `dart`, `swift`, or `package` for the cross-build producer targets) - #[arg(long)] - target: Option, - /// Output format: `json` for machine-readable output, otherwise human-readable - #[arg(long)] - format: Option, - }, + /// Print shell completions Completions { /// Shell to generate completions for shell: clap_complete::Shell, }, - Man { - /// Output directory for generated man pages - #[arg(long)] - out: String, - }, + /// Print the IDL schema version this build reads and writes SchemaVersion, - Watch { - /// Input IDL/IR file (yaml|yml|json|toml) - input: String, - /// Output directory for generated artifacts - #[arg(short, long, default_value = "./generated")] - out: String, - /// Comma-separated list of targets to generate - #[arg(short, long)] - target: Option, - /// Path to a TOML configuration file for generator options - #[arg(long)] - config: Option, - }, - Format { - /// Input IDL/IR file (yaml|yml|json|toml) - input: String, - /// Exit non-zero if the file is not already canonically formatted - #[arg(long)] - check: bool, - }, + /// Print the IDL document schema Schema { /// Schema export format (currently only json-schema is supported) #[arg(long, default_value = "json-schema")] @@ -214,12 +176,10 @@ fn main() -> Result<()> { let quiet = cli.quiet; match cli.command { - Commands::New { name } => commands::new::cmd_new(&name, quiet)?, Commands::Generate { input, out, target, - scaffold, config, warn, force, @@ -228,7 +188,6 @@ fn main() -> Result<()> { &input, &out, target.as_deref(), - scaffold, config.as_deref(), warn, force, @@ -277,27 +236,14 @@ fn main() -> Result<()> { warn, quiet, )?, - Commands::Lint { input, format } => { - if !commands::validate::cmd_lint(&input, format.map(HumanJsonFormat::as_str), quiet)? { - std::process::exit(1); - } - } - Commands::Diff { input, out, check } => { - commands::diff::cmd_diff(&input, out.as_deref(), check, quiet)? - } - Commands::Doctor { target, format } => { - doctor::cmd_doctor(target.as_deref(), format.map(HumanJsonFormat::as_str))? - } - Commands::Completions { shell } => cmd_completions(shell), - Commands::Man { out } => cmd_man(&out, quiet)?, - Commands::SchemaVersion => println!("{CURRENT_SCHEMA_VERSION}"), - Commands::Watch { + Commands::Diff { input, out, - target, config, - } => commands::watch::cmd_watch(&input, &out, target.as_deref(), config.as_deref(), quiet)?, - Commands::Format { input, check } => commands::format::cmd_format(&input, check, quiet)?, + check, + } => commands::diff::cmd_diff(&input, out.as_deref(), config.as_deref(), check, quiet)?, + Commands::Completions { shell } => cmd_completions(shell), + Commands::SchemaVersion => println!("{CURRENT_SCHEMA_VERSION}"), Commands::Schema { format } => cmd_schema(&format)?, } Ok(()) @@ -312,23 +258,6 @@ fn cmd_completions(shell: clap_complete::Shell) { ); } -fn cmd_man(out: &str, quiet: bool) -> Result<()> { - let out_dir = std::path::PathBuf::from(out); - std::fs::create_dir_all(&out_dir) - .into_diagnostic() - .wrap_err_with(|| format!("failed to create directory: {out}"))?; - - clap_mangen::generate_to(Cli::command(), &out_dir) - .into_diagnostic() - .wrap_err("failed to generate man pages")?; - - if !quiet { - println!("Man pages written to {}", out_dir.display()); - } - - Ok(()) -} - fn cmd_schema(format: &str) -> Result<()> { match format { "json-schema" => { @@ -352,167 +281,40 @@ mod tests { #[test] fn rejects_unknown_human_json_output_formats() { - for args in [ - &["weaveffi", "validate", "input.yml", "--format", "jsonn"][..], - &["weaveffi", "lint", "input.yml", "--format", "xml"], - &["weaveffi", "doctor", "--target", "swift", "--format", "xml"], - ] { - let error = Cli::try_parse_from(args).expect_err("unknown format should be rejected"); - assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue); - let message = error.to_string(); - assert!( - message.contains("possible values: human, json"), - "{message}" - ); - } - } - - #[test] - fn doctor_checks_cross_targets() { - let cmd = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .arg("doctor") - .output() - .expect("failed to run weaveffi doctor"); - - let stdout = String::from_utf8_lossy(&cmd.stdout); - assert!(cmd.status.success(), "doctor failed: {stdout}"); - assert!( - stdout.contains("Cross-compilation targets:"), - "missing cross-target section in doctor output: {stdout}" - ); - assert!( - stdout.contains("aarch64-apple-ios"), - "missing iOS target check: {stdout}" - ); - assert!( - stdout.contains("aarch64-linux-android"), - "missing Android target check: {stdout}" - ); - assert!( - stdout.contains("wasm32-unknown-unknown"), - "missing Wasm target check: {stdout}" - ); - assert!( - stdout.contains("WebAssembly tools:"), - "missing wasm tools section: {stdout}" - ); - assert!( - stdout.contains("wasm-pack"), - "missing wasm-pack check: {stdout}" - ); + let args = ["weaveffi", "validate", "input.yml", "--format", "jsonn"]; + let error = Cli::try_parse_from(args).expect_err("unknown format should be rejected"); + assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue); assert!( - stdout.contains("wasm-bindgen-cli"), - "missing wasm-bindgen-cli check: {stdout}" + error.to_string().contains("possible values: human, json"), + "{error}" ); } #[test] - fn completions_bash() { - let cmd = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args(["completions", "bash"]) - .output() - .expect("failed to run weaveffi completions bash"); - - let stdout = String::from_utf8_lossy(&cmd.stdout); - assert!(cmd.status.success(), "completions bash failed: {stdout}"); - assert!( - stdout.contains("weaveffi"), - "bash completions should reference weaveffi: {stdout}" - ); - assert!( - stdout.contains("complete"), - "bash completions should contain 'complete': {stdout}" - ); - } - - #[test] - fn completions_zsh() { - let cmd = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args(["completions", "zsh"]) - .output() - .expect("failed to run weaveffi completions zsh"); - - let stdout = String::from_utf8_lossy(&cmd.stdout); - assert!(cmd.status.success(), "completions zsh failed: {stdout}"); - assert!( - stdout.contains("weaveffi"), - "zsh completions should reference weaveffi: {stdout}" - ); - assert!( - stdout.contains("compdef"), - "zsh completions should contain 'compdef': {stdout}" - ); - } - - #[test] - fn schema_version_prints_current() { - let cmd = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .arg("schema-version") - .output() - .expect("failed to run weaveffi schema-version"); - - let stdout = String::from_utf8_lossy(&cmd.stdout); - assert!(cmd.status.success(), "schema-version failed: {stdout}"); - assert_eq!(stdout.trim(), CURRENT_SCHEMA_VERSION); + fn removed_subcommands_are_gone() { + for cmd in ["new", "lint", "doctor", "man", "watch", "format"] { + assert!( + Cli::try_parse_from(["weaveffi", cmd]).is_err(), + "`{cmd}` should no longer parse" + ); + } } #[test] - fn man_generation() { - let temp_dir = tempfile::tempdir().expect("failed to create temp dir"); - let out_dir = temp_dir.path().join("man"); - - let cmd = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args(["man", "--out", out_dir.to_str().unwrap()]) - .output() - .expect("failed to run weaveffi man"); - - let stdout = String::from_utf8_lossy(&cmd.stdout); - assert!(cmd.status.success(), "man generation failed: {stdout}"); - assert!( - stdout.contains("Man pages written to"), - "should print where pages were written (not quiet): {stdout}" - ); - - // Verify top-level - let main_page = out_dir.join("weaveffi.1"); - assert!(main_page.exists(), "weaveffi.1 not generated"); - assert!( - std::fs::metadata(&main_page).unwrap().len() > 0, - "weaveffi.1 is empty" - ); - - // Verify subcommands exist. At minimum we know `generate` and `doctor` exist. - let generate_page = out_dir.join("weaveffi-generate.1"); - assert!(generate_page.exists(), "weaveffi-generate.1 not generated"); - assert!( - std::fs::metadata(&generate_page).unwrap().len() > 0, - "weaveffi-generate.1 is empty" - ); - - let doctor_page = out_dir.join("weaveffi-doctor.1"); - assert!(doctor_page.exists(), "weaveffi-doctor.1 not generated"); - assert!( - std::fs::metadata(&doctor_page).unwrap().len() > 0, - "weaveffi-doctor.1 is empty" - ); - - // Verify quiet respects global flag - let quiet_cmd = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args(["--quiet", "man", "--out", out_dir.to_str().unwrap()]) - .output() - .expect("failed to run weaveffi --quiet man"); - - let quiet_stdout = String::from_utf8_lossy(&quiet_cmd.stdout); - assert!(quiet_cmd.status.success()); - assert!( - !quiet_stdout.contains("Man pages written to"), - "quiet mode should suppress output" - ); + fn completions_and_schema_version() { + for (args, needle) in [ + (&["completions", "bash"][..], "complete"), + (&["completions", "zsh"][..], "compdef"), + (&["schema-version"][..], CURRENT_SCHEMA_VERSION), + ] { + let cmd = assert_cmd::Command::cargo_bin("weaveffi") + .expect("binary not found") + .args(args) + .output() + .expect("failed to run weaveffi"); + let stdout = String::from_utf8_lossy(&cmd.stdout); + assert!(cmd.status.success(), "{args:?} failed: {stdout}"); + assert!(stdout.contains(needle), "{args:?}: {stdout}"); + } } } diff --git a/crates/weaveffi-cli/src/scaffold.rs b/crates/weaveffi-cli/src/scaffold.rs deleted file mode 100644 index f9473917..00000000 --- a/crates/weaveffi-cli/src/scaffold.rs +++ /dev/null @@ -1,1218 +0,0 @@ -use std::fmt::Write; - -use weaveffi_core::abi::AbiParam; -use weaveffi_core::model::{AbiFn, BindingModel, CallShape, ModuleBinding}; -use weaveffi_core::resolved::ResolvedApi; - -/// The body every generated stub carries until the producer fills it in. -const TODO_BODY: &str = " todo!()\n"; - -/// Render `name: ` for one lowered ABI slot. The interface -/// receiver slot is named `self` in the C header; that is a keyword in Rust, -/// so the stub renames it `self_`. -fn slot_decl(p: &AbiParam, prefix: &str) -> String { - let name = if p.name == "self" { "self_" } else { &p.name }; - format!("{}: {}", name, p.ty.render_rust(prefix)) -} - -/// Join lowered ABI slots into a Rust parameter list. -fn slots_decl(params: &[AbiParam], prefix: &str) -> String { - params - .iter() - .map(|p| slot_decl(p, prefix)) - .collect::>() - .join(", ") -} - -/// Emit a `#[no_mangle] pub extern "C"` stub from an explicit signature. -fn emit_stub(out: &mut String, symbol: &str, params: &str, ret: &str, body: &str) { - out.push_str("#[no_mangle]\n"); - let _ = writeln!(out, "pub extern \"C\" fn {symbol}({params}){ret} {{"); - out.push_str(body); - out.push_str("}\n\n"); -} - -/// Emit a stub directly from a lowered [`AbiFn`]. A `void` return omits the -/// `-> T`; every other return type is rendered via the shared Rust vocabulary. -fn emit_abi_fn(out: &mut String, f: &AbiFn, prefix: &str, body: &str) { - let ret = if f.ret == weaveffi_core::abi::CType::Void { - String::new() - } else { - format!(" -> {}", f.ret.render_rust(prefix)) - }; - emit_stub(out, &f.symbol, &slots_decl(&f.params, prefix), &ret, body); -} - -/// Render Rust `#[no_mangle] extern "C"` producer stubs for every symbol the -/// generated C ABI exposes, with `todo!()` bodies for the author to fill in. -/// -/// Every signature is lowered through the shared [`BindingModel`] and -/// [`render_rust`](weaveffi_core::abi::CType::render_rust), so a scaffolded -/// producer matches the generated header (and therefore every language binding) -/// by construction; there is no second, drift-prone ABI lowering here. -pub fn render_scaffold(api: &ResolvedApi, c_prefix: &str) -> String { - let model = BindingModel::build(api, c_prefix); - let mut out = String::new(); - out.push_str("#![allow(unsafe_code)]\n"); - out.push_str("#![allow(clippy::not_unsafe_ptr_arg_deref)]\n\n"); - out.push_str("use std::os::raw::c_char;\n"); - out.push_str("use weaveffi_abi::{self as abi, *};\n\n"); - - // `BindingModel::modules` is already flattened in pre-order, so a single - // pass covers nested modules without re-deriving the joined path. - for m in &model.modules { - render_module(&mut out, m, c_prefix); - } - - out.push_str("// Re-export the fixed WeaveFFI C ABI runtime surface\n"); - out.push_str("// (string/byte deallocation, error clearing, cancel-token lifecycle)\n"); - out.push_str("// so consumer wrappers can call into this cdylib.\n"); - out.push_str("abi::export_runtime!();\n"); - - out -} - -fn render_module(out: &mut String, m: &ModuleBinding, prefix: &str) { - // Records and rich enums are value types with no producer C surface: - // their encodings arrive and leave through the buffered `(ptr, len)` - // slots already present on the function stubs below. - // Module-scope callback function-pointer typedefs the producer invokes. - for cb in &m.callbacks { - let _ = writeln!( - out, - "pub type {} = extern \"C\" fn({});\n", - cb.c_fn_type, - slots_decl(&cb.abi_params, prefix) - ); - } - // Event listeners: a register/unregister pair bound to a callback. - for l in &m.listeners { - emit_stub( - out, - &l.register_symbol, - &format!( - "callback: {}, context: *mut std::ffi::c_void", - l.callback_c_fn_type - ), - " -> u64", - TODO_BODY, - ); - emit_stub(out, &l.unregister_symbol, "id: u64", "", TODO_BODY); - } - for i in &m.interfaces { - render_interface_scaffold(out, i, prefix); - } - for f in &m.functions { - emit_callable(out, &f.shape, prefix); - } -} - -/// Emit the producer stubs for one lowered call shape: a plain symbol, an -/// async callback typedef plus launcher, or an iterator's opaque state type -/// with its `launch`/`next`/`destroy` triple. -fn emit_callable(out: &mut String, shape: &CallShape, prefix: &str) { - match shape { - CallShape::Sync(abi) => emit_abi_fn(out, abi, prefix, TODO_BODY), - CallShape::Async(a) => { - // The completion callback typedef, then the launcher (its - // params already carry the callback + context slots). - let _ = writeln!( - out, - "pub type {} = extern \"C\" fn({});\n", - a.callback_type, - slots_decl(&a.callback_params, prefix) - ); - emit_abi_fn( - out, - &a.launch, - prefix, - " todo!(\"spawn async work and call callback with result\")\n", - ); - } - CallShape::Iterator(it) => { - let tag = &it.iter_tag; - let _ = writeln!(out, "#[repr(C)]\npub struct {tag} {{"); - out.push_str(" // TODO: hold the iterator's streaming state\n"); - out.push_str("}\n\n"); - emit_abi_fn(out, &it.launch, prefix, TODO_BODY); - emit_abi_fn(out, &it.next, prefix, TODO_BODY); - emit_stub( - out, - &it.destroy_symbol, - &format!("iter: *mut {tag}"), - "", - TODO_BODY, - ); - } - } -} - -/// Emit the producer surface for an interface: the opaque object type, every -/// constructor (returning an owned `*mut {tag}`), every method (leading -/// `self_: *const {tag}` receiver slot), every static, and the -/// `{tag}_destroy` release hook. Symbols and signatures come straight from -/// the [`InterfaceBinding`](weaveffi_core::model::InterfaceBinding), -/// mirroring the generated C header exactly. -fn render_interface_scaffold( - out: &mut String, - i: &weaveffi_core::model::InterfaceBinding, - prefix: &str, -) { - let tag = &i.c_tag; - let _ = writeln!(out, "#[repr(C)]\npub struct {tag} {{"); - out.push_str(" // TODO: hold the object's state\n"); - out.push_str("}\n\n"); - for c in &i.constructors { - emit_callable(out, &c.shape, prefix); - } - for m in &i.methods { - emit_callable(out, &m.shape, prefix); - } - for s in &i.statics { - emit_callable(out, &s.shape, prefix); - } - emit_stub( - out, - &i.destroy_symbol, - &format!("self_: *mut {tag}"), - "", - TODO_BODY, - ); -} - -#[cfg(test)] -mod tests { - use super::*; - use weaveffi_ir::ir::{Api, Function, Module, Param, StructDef, StructField, TypeRef}; - - fn minimal_api(functions: Vec, structs: Vec) -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "calc".to_string(), - functions, - structs, - enums: vec![], - interfaces: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }) - } - - #[test] - fn scaffold_interface_emits_type_members_and_destroy() { - use weaveffi_ir::ir::InterfaceDef; - let mut api = minimal_api(vec![], vec![]).api().clone(); - api.modules[0].interfaces.push(InterfaceDef { - name: "Store".into(), - doc: None, - constructors: vec![Function { - name: "open".into(), - params: vec![Param { - name: "path".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - cancellable: false, - throws: true, - deprecated: None, - since: None, - }], - methods: vec![Function { - name: "count".into(), - params: vec![], - returns: Some(TypeRef::I64), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - statics: vec![], - }); - let api = ResolvedApi::assume_resolved(api); - let out = render_scaffold(&api, "weaveffi"); - assert!(out.contains("pub struct weaveffi_calc_Store {")); - assert!(out.contains("fn weaveffi_calc_Store_open(")); - // The method's receiver slot is renamed self_ for Rust (self is a - // keyword), while the C header keeps `self`. - assert!( - out.contains("fn weaveffi_calc_Store_count(self_: *const weaveffi_calc_Store"), - "method stub should carry a renamed receiver slot: {out}" - ); - assert!(out.contains("fn weaveffi_calc_Store_destroy(self_: *mut weaveffi_calc_Store)")); - } - - #[test] - fn scaffold_has_allow_unsafe() { - let api = minimal_api(vec![], vec![]); - let out = render_scaffold(&api, "weaveffi"); - assert!(out.contains("#![allow(unsafe_code)]")); - } - - #[test] - fn scaffold_imports_abi() { - let api = minimal_api(vec![], vec![]); - let out = render_scaffold(&api, "weaveffi"); - assert!(out.contains("use weaveffi_abi::{self as abi, *};")); - } - - #[test] - fn scaffold_includes_runtime_exports_via_macro() { - let api = minimal_api(vec![], vec![]); - let out = render_scaffold(&api, "weaveffi"); - assert!( - out.contains("abi::export_runtime!();"), - "scaffold must call the export_runtime! macro instead of hand-writing thunks: {out}" - ); - assert!( - !out.contains("fn weaveffi_free_string("), - "scaffold should not hand-write runtime thunks; the macro emits them: {out}" - ); - assert!( - !out.contains("fn weaveffi_free_bytes("), - "scaffold should not hand-write runtime thunks; the macro emits them: {out}" - ); - assert!( - !out.contains("fn weaveffi_error_clear("), - "scaffold should not hand-write runtime thunks; the macro emits them: {out}" - ); - } - - #[test] - fn scaffold_custom_prefix_renames_user_symbols() { - let api = minimal_api( - vec![Function { - name: "add".into(), - params: vec![Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - vec![StructDef { - name: "Point".into(), - doc: None, - fields: vec![StructField { - name: "x".into(), - ty: TypeRef::F64, - doc: None, - }], - }], - ); - let out = render_scaffold(&api, "myffi"); - assert!( - out.contains("pub extern \"C\" fn myffi_calc_add("), - "user fn should adopt custom prefix: {out}" - ); - assert!( - !out.contains("weaveffi_calc_add"), - "user fn should not retain default prefix: {out}" - ); - assert!( - out.contains("abi::export_runtime!();"), - "runtime exports must come from the export_runtime! macro: {out}" - ); - } - - #[test] - fn scaffold_function_i32() { - let api = minimal_api( - vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - vec![], - ); - let out = render_scaffold(&api, "weaveffi"); - assert!( - out.contains( - "pub extern \"C\" fn weaveffi_calc_add(a: i32, b: i32, out_err: *mut weaveffi_error) -> i32 {" - ), - "missing add stub: {out}" - ); - assert!(out.contains("todo!()")); - } - - #[test] - fn scaffold_function_string_param_and_return() { - let api = minimal_api( - vec![Function { - name: "echo".into(), - params: vec![Param { - name: "s".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - vec![], - ); - let out = render_scaffold(&api, "weaveffi"); - assert!( - out.contains( - "pub extern \"C\" fn weaveffi_calc_echo(s: *const c_char, out_err: *mut weaveffi_error) -> *const c_char {" - ), - "missing echo stub: {out}" - ); - } - - #[test] - fn scaffold_function_bytes_return_has_out_len() { - let api = minimal_api( - vec![Function { - name: "data".into(), - params: vec![], - returns: Some(TypeRef::Bytes), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - vec![], - ); - let out = render_scaffold(&api, "weaveffi"); - assert!( - out.contains("out_len: *mut usize"), - "bytes return should add out_len param: {out}" - ); - // Matches the C header's `const uint8_t*` return exactly (the producer - // hands back a buffer the consumer frees via `weaveffi_free_bytes`). - assert!( - out.contains("-> *const u8"), - "bytes return type should be *const u8: {out}" - ); - } - - #[test] - fn scaffold_void_function() { - let api = minimal_api( - vec![Function { - name: "reset".into(), - params: vec![], - returns: None, - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - vec![], - ); - let out = render_scaffold(&api, "weaveffi"); - assert!( - out.contains("pub extern \"C\" fn weaveffi_calc_reset(out_err: *mut weaveffi_error) {"), - "missing void function: {out}" - ); - } - - #[test] - fn scaffold_struct_emits_no_producer_surface() { - // Records are value types crossing the ABI as serialized buffers: - // there is no opaque object, create/destroy pair, or getter surface - // for the producer to implement. - let api = minimal_api( - vec![], - vec![StructDef { - name: "Point".into(), - doc: None, - fields: vec![ - StructField { - name: "x".into(), - ty: TypeRef::F64, - doc: None, - }, - StructField { - name: "y".into(), - ty: TypeRef::F64, - doc: None, - }, - ], - }], - ); - let out = render_scaffold(&api, "weaveffi"); - assert!( - !out.contains("weaveffi_calc_Point"), - "records must not get producer stubs: {out}" - ); - } - - #[test] - fn scaffold_record_param_and_return_are_buffered() { - let api = minimal_api( - vec![Function { - name: "save".into(), - params: vec![Param { - name: "point".into(), - ty: TypeRef::Record("Point".into()), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Point".into())), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - vec![StructDef { - name: "Point".into(), - doc: None, - fields: vec![StructField { - name: "x".into(), - ty: TypeRef::F64, - doc: None, - }], - }], - ); - let out = render_scaffold(&api, "weaveffi"); - assert!( - out.contains("point_ptr: *const u8, point_len: usize"), - "record param should be a borrowed value buffer: {out}" - ); - assert!( - out.contains("out_len: *mut usize") && out.contains("-> *const u8"), - "record return should be a value buffer with out_len: {out}" - ); - } - - #[test] - fn scaffold_rich_enum_emits_no_producer_surface() { - use weaveffi_ir::ir::{EnumDef, EnumVariant}; - let api = ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules: vec![Module { - name: "shapes".into(), - functions: vec![], - structs: vec![], - enums: vec![EnumDef { - name: "Shape".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Empty".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Circle".into(), - value: 1, - doc: None, - fields: vec![StructField { - name: "radius".into(), - ty: TypeRef::F64, - doc: None, - }], - }, - ], - }], - interfaces: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }); - let out = render_scaffold(&api, "weaveffi"); - // Rich enums are value types crossing the ABI as serialized buffers: - // no opaque object, per-variant constructors, getters, tag reader, or - // destructor for the producer to implement. - assert!( - !out.contains("weaveffi_shapes_Shape"), - "rich enums must not get producer stubs: {out}" - ); - } - - #[test] - fn scaffold_c_style_enum_emits_no_producer_surface() { - use weaveffi_ir::ir::{EnumDef, EnumVariant}; - let api = ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules: vec![Module { - name: "shapes".into(), - functions: vec![], - structs: vec![], - enums: vec![EnumDef { - name: "Channel".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }], - interfaces: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }); - let out = render_scaffold(&api, "weaveffi"); - assert!( - !out.contains("weaveffi_shapes_Channel"), - "plain C-style enum must not get producer stubs: {out}" - ); - } - - #[test] - fn scaffold_optional_value_param_is_buffered() { - let api = minimal_api( - vec![Function { - name: "find".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Optional(Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - vec![], - ); - let out = render_scaffold(&api, "weaveffi"); - assert!( - out.contains("id_ptr: *const u8, id_len: usize"), - "optional param should be a borrowed value buffer: {out}" - ); - } - - #[test] - fn scaffold_list_param_is_buffered() { - let api = minimal_api( - vec![Function { - name: "sum".into(), - params: vec![Param { - name: "items".into(), - ty: TypeRef::List(Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - vec![], - ); - let out = render_scaffold(&api, "weaveffi"); - assert!( - out.contains("items_ptr: *const u8, items_len: usize"), - "list param should be a borrowed value buffer: {out}" - ); - } - - #[test] - fn scaffold_enum_param_uses_i32() { - let api = minimal_api( - vec![Function { - name: "paint".into(), - params: vec![Param { - name: "color".into(), - ty: TypeRef::Enum("Color".into()), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Enum("Color".into())), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - vec![], - ); - let out = render_scaffold(&api, "weaveffi"); - assert!( - out.contains("color: i32"), - "enum param should be i32: {out}" - ); - assert!(out.contains("-> i32"), "enum return should be i32: {out}"); - } - - #[test] - fn scaffold_all_functions_have_no_mangle() { - let api = minimal_api( - vec![Function { - name: "add".into(), - params: vec![], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - vec![], - ); - let out = render_scaffold(&api, "weaveffi"); - let no_mangle_count = out.matches("#[no_mangle]").count(); - let extern_count = out.matches("pub extern \"C\"").count(); - assert_eq!( - no_mangle_count, extern_count, - "every extern fn should have #[no_mangle]" - ); - } - - #[test] - fn scaffold_handle_type() { - let api = minimal_api( - vec![Function { - name: "open".into(), - params: vec![], - returns: Some(TypeRef::Handle), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - vec![], - ); - let out = render_scaffold(&api, "weaveffi"); - assert!(out.contains("-> u64"), "handle return should be u64: {out}"); - } - - #[test] - fn scaffold_with_map_type() { - let api = minimal_api( - vec![Function { - name: "get_scores".into(), - params: vec![Param { - name: "grades".into(), - ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - vec![], - ); - let out = render_scaffold(&api, "weaveffi"); - // Maps are value buffers in both directions: one borrowed `(ptr, len)` - // pair in, one owned buffer plus `out_len` back. - assert!( - out.contains("grades_ptr: *const u8, grades_len: usize"), - "map param should be a borrowed value buffer: {out}" - ); - assert!( - out.contains("out_len: *mut usize"), - "map return should use the canonical out_len: {out}" - ); - assert!( - out.contains("-> *const u8"), - "map return should be an owned value buffer: {out}" - ); - } - - #[test] - fn scaffold_typed_handle() { - let api = minimal_api( - vec![Function { - name: "close".into(), - params: vec![Param { - name: "contact".into(), - ty: TypeRef::TypedHandle("Contact".into()), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::TypedHandle("Contact".into())), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - vec![], - ); - let out = render_scaffold(&api, "weaveffi"); - // A typed handle crosses the ABI as the module's opaque tag pointer, - // exactly as the header forward-declares it. - assert!( - out.contains("contact: *mut weaveffi_calc_Contact"), - "TypedHandle param should be the opaque tag pointer: {out}" - ); - assert!( - out.contains("-> *mut weaveffi_calc_Contact"), - "TypedHandle return should be the opaque tag pointer: {out}" - ); - } - - #[test] - fn scaffold_async_function() { - let api = minimal_api( - vec![Function { - name: "fetch".into(), - params: vec![Param { - name: "url".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - r#async: true, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - vec![], - ); - let out = render_scaffold(&api, "weaveffi"); - assert!( - out.contains("pub type weaveffi_calc_fetch_callback = extern \"C\" fn("), - "missing callback type alias: {out}" - ); - assert!( - out.contains("callback: weaveffi_calc_fetch_callback"), - "missing callback parameter: {out}" - ); - assert!( - out.contains("context: *mut std::ffi::c_void"), - "missing context parameter: {out}" - ); - assert!( - out.contains("todo!(\"spawn async work and call callback with result\")"), - "missing async todo body: {out}" - ); - assert!( - !out.contains("out_err: *mut weaveffi_error"), - "async function should not have out_err param: {out}" - ); - assert!( - !out.contains("-> *const c_char"), - "async function should not have a return type: {out}" - ); - } - - #[test] - fn scaffold_async_void_function() { - let api = minimal_api( - vec![Function { - name: "sync_data".into(), - params: vec![], - returns: None, - doc: None, - r#async: true, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - vec![], - ); - let out = render_scaffold(&api, "weaveffi"); - // The callback prefix is `(context, err)`, matching the header's - // `typedef void (*..)(void* context, weaveffi_error* err)`. The earlier - // hand-rolled scaffold emitted these in the reverse order. - assert!( - out.contains( - "pub type weaveffi_calc_sync_data_callback = extern \"C\" fn(context: *mut std::ffi::c_void, err: *mut weaveffi_error);" - ), - "void async callback should be (context, err): {out}" - ); - assert!( - out.contains("callback: weaveffi_calc_sync_data_callback"), - "missing callback parameter: {out}" - ); - } - - #[test] - fn scaffold_recurses_into_nested_modules_with_joined_path() { - // A nested submodule's functions/structs must get stubs, and their C - // symbols must use the underscore-joined module path so they line up - // with the generated bindings. - let api = ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules: vec![Module { - name: "graphics".into(), - functions: vec![], - structs: vec![], - enums: vec![], - interfaces: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![Module { - name: "shapes".into(), - functions: vec![Function { - name: "make".into(), - params: vec![], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Shape".into(), - doc: None, - fields: vec![StructField { - name: "sides".into(), - ty: TypeRef::I32, - doc: None, - }], - }], - enums: vec![], - interfaces: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - }], - generators: None, - package: None, - }); - let out = render_scaffold(&api, "weaveffi"); - assert!( - out.contains("pub extern \"C\" fn weaveffi_graphics_shapes_make("), - "nested fn should use joined path: {out}" - ); - // The nested record is a value type: no producer stubs of its own. - assert!( - !out.contains("weaveffi_graphics_shapes_Shape"), - "nested record must not get producer stubs: {out}" - ); - } - - #[test] - fn scaffold_qualified_cross_module_struct_ref_mangles_correctly() { - // After resolution, a cross-module struct reference is dot-qualified - // (e.g. `shared.Token`). The scaffold must flatten it to the owning - // module's C symbol, never embed the dot or the referrer's module. - let api = ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules: vec![Module { - name: "kitchen".into(), - functions: vec![Function { - name: "cross".into(), - params: vec![], - returns: Some(TypeRef::Record("shared.Token".into())), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - interfaces: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }); - let out = render_scaffold(&api, "weaveffi"); - // A cross-module record return is a value buffer like any other; the - // stub must not leak the dotted name or invent a referrer-module tag. - assert!( - out.contains("out_len: *mut usize") && out.contains("-> *const u8"), - "cross-module record return should be a value buffer: {out}" - ); - assert!( - !out.contains("weaveffi_kitchen_shared"), - "qualified ref must not embed the referrer module: {out}" - ); - assert!( - !out.contains("shared.Token"), - "qualified ref must not leak the dotted name into Rust: {out}" - ); - } - - #[test] - fn scaffold_multiple_modules() { - let api = ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules: vec![ - Module { - name: "math".into(), - functions: vec![Function { - name: "add".into(), - params: vec![], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - interfaces: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }, - Module { - name: "io".into(), - functions: vec![Function { - name: "read".into(), - params: vec![], - returns: Some(TypeRef::StringUtf8), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - interfaces: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }, - ], - generators: None, - package: None, - }); - let out = render_scaffold(&api, "weaveffi"); - assert!(out.contains("weaveffi_math_add"), "missing math module"); - assert!(out.contains("weaveffi_io_read"), "missing io module"); - } - - fn events_module( - functions: Vec, - callbacks: Vec, - listeners: Vec, - ) -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules: vec![Module { - name: "events".into(), - functions, - structs: vec![], - enums: vec![], - interfaces: vec![], - callbacks, - listeners, - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }) - } - - #[test] - fn scaffold_iterator_emits_opaque_type_next_and_destroy() { - let api = events_module( - vec![Function { - name: "get_messages".into(), - params: vec![], - returns: Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - vec![], - vec![], - ); - let out = render_scaffold(&api, "weaveffi"); - // Opaque iterator object the producer fills with streaming state. - assert!( - out.contains("pub struct weaveffi_events_GetMessagesIterator {"), - "iterator opaque type missing: {out}" - ); - // Launcher returns the opaque iterator pointer. - assert!( - out.contains("pub extern \"C\" fn weaveffi_events_get_messages(out_err: *mut weaveffi_error) -> *mut weaveffi_events_GetMessagesIterator {"), - "iterator launcher missing/incorrect: {out}" - ); - // `next` writes the element through `out_item` and returns a status int. - assert!( - out.contains("pub extern \"C\" fn weaveffi_events_GetMessagesIterator_next(iter: *mut weaveffi_events_GetMessagesIterator, out_item: *mut *const c_char, out_err: *mut weaveffi_error) -> i32 {"), - "iterator next missing/incorrect: {out}" - ); - assert!( - out.contains("pub extern \"C\" fn weaveffi_events_GetMessagesIterator_destroy(iter: *mut weaveffi_events_GetMessagesIterator) {"), - "iterator destroy missing/incorrect: {out}" - ); - } - - #[test] - fn scaffold_listener_emits_callback_typedef_and_register_pair() { - use weaveffi_ir::ir::{CallbackDef, ListenerDef}; - let api = events_module( - vec![], - vec![CallbackDef { - name: "on_message".into(), - params: vec![Param { - name: "text".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - doc: None, - }], - vec![ListenerDef { - name: "messages".into(), - event_callback: "on_message".into(), - doc: None, - }], - ); - let out = render_scaffold(&api, "weaveffi"); - assert!( - out.contains("pub type weaveffi_events_on_message_fn = extern \"C\" fn(text: *const c_char, context: *mut std::ffi::c_void);"), - "module callback typedef missing/incorrect: {out}" - ); - assert!( - out.contains("pub extern \"C\" fn weaveffi_events_register_messages(callback: weaveffi_events_on_message_fn, context: *mut std::ffi::c_void) -> u64 {"), - "listener register missing/incorrect: {out}" - ); - assert!( - out.contains("pub extern \"C\" fn weaveffi_events_unregister_messages(id: u64) {"), - "listener unregister missing/incorrect: {out}" - ); - } - - #[test] - fn scaffold_cancellable_async_threads_cancel_token() { - let api = ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules: vec![Module { - name: "net".into(), - functions: vec![Function { - name: "fetch".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I64, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - r#async: true, - cancellable: true, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - interfaces: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }); - let out = render_scaffold(&api, "weaveffi"); - // The completion callback prefix is (context, err, result). - assert!( - out.contains("pub type weaveffi_net_fetch_callback = extern \"C\" fn(context: *mut std::ffi::c_void, err: *mut weaveffi_error, result: *const c_char);"), - "async callback typedef missing/incorrect: {out}" - ); - // The cancel token slot sits before callback/context on the launcher. - assert!( - out.contains("cancel_token: *mut weaveffi_cancel_token"), - "cancellable async must thread a cancel token: {out}" - ); - assert!( - out.contains("pub extern \"C\" fn weaveffi_net_fetch_async(id: i64, cancel_token: *mut weaveffi_cancel_token, callback: weaveffi_net_fetch_callback, context: *mut std::ffi::c_void) {"), - "async launcher signature missing/incorrect: {out}" - ); - } -} diff --git a/crates/weaveffi-cli/tests/cli_async.rs b/crates/weaveffi-cli/tests/cli_async.rs deleted file mode 100644 index 1ca86bc5..00000000 --- a/crates/weaveffi-cli/tests/cli_async.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::path::Path; - -#[test] -fn generate_async_demo_all_targets() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let input = repo_root.join("samples/async-demo/async_demo.yml"); - - 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") - .args([ - "generate", - input.to_str().unwrap(), - "-o", - out_path.to_str().unwrap(), - ]) - .assert() - .success(); - - let node_dts = - std::fs::read_to_string(out_path.join("node/types.d.ts")).expect("missing node/types.d.ts"); - assert!( - node_dts.contains("Promise<"), - "node/types.d.ts should contain Promise for async functions" - ); - - let swift = std::fs::read_to_string(out_path.join("swift/Sources/AsyncDemo/AsyncDemo.swift")) - .expect("missing swift/Sources/AsyncDemo/AsyncDemo.swift"); - assert!( - swift.contains("async throws"), - "WeaveFFI.swift should contain async throws for async functions" - ); - - let kotlin = - std::fs::read_to_string(out_path.join("android/src/main/kotlin/com/weaveffi/WeaveFFI.kt")) - .expect("missing android WeaveFFI.kt"); - assert!( - kotlin.contains("suspend"), - "WeaveFFI.kt should contain suspend for async functions" - ); - - let python = std::fs::read_to_string(out_path.join("python/async_demo/weaveffi.py")) - .expect("missing python/async_demo/weaveffi.py"); - assert!( - python.contains("async def"), - "weaveffi.py should contain async def for async functions" - ); - - let dotnet = std::fs::read_to_string(out_path.join("dotnet/WeaveFFI.cs")) - .expect("missing dotnet/WeaveFFI.cs"); - assert!( - dotnet.contains("async Task<"), - "WeaveFFI.cs should contain async Task for async functions" - ); - - let wasm_dts = std::fs::read_to_string(out_path.join("wasm/weaveffi_wasm.d.ts")) - .expect("missing wasm/weaveffi_wasm.d.ts"); - assert!( - wasm_dts.contains("Promise<"), - "weaveffi_wasm.d.ts should contain Promise for async functions" - ); - - let c_header = - std::fs::read_to_string(out_path.join("c/weaveffi.h")).expect("missing c/weaveffi.h"); - assert!( - c_header.contains("_async"), - "weaveffi.h should contain _async suffix for async functions" - ); - assert!( - c_header.contains("callback"), - "weaveffi.h should contain callback typedefs for async functions" - ); -} diff --git a/crates/weaveffi-cli/tests/cli_c_prefix.rs b/crates/weaveffi-cli/tests/cli_c_prefix.rs deleted file mode 100644 index cf820313..00000000 --- a/crates/weaveffi-cli/tests/cli_c_prefix.rs +++ /dev/null @@ -1,167 +0,0 @@ -use std::fs; -use std::path::{Path, PathBuf}; - -fn repo_root() -> PathBuf { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - Path::new(manifest_dir) - .parent() - .unwrap() - .parent() - .unwrap() - .to_path_buf() -} - -/// Recursively read every file under `dir` and concatenate the text contents. -fn read_tree(dir: &Path) -> String { - let mut out = String::new(); - let entries = match fs::read_dir(dir) { - Ok(e) => e, - Err(_) => return out, - }; - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - out.push_str(&read_tree(&path)); - } else if let Ok(contents) = fs::read_to_string(&path) { - out.push_str(&contents); - out.push('\n'); - } - } - out -} - -/// The C ABI prefix must reach **every** language backend, not just C/C++. -/// Before the unified `c_prefix` plumbing, nine of the eleven generators -/// hard-coded `weaveffi_`, so a custom prefix produced consumer code that -/// linked against symbols the (re-prefixed) producer never exported. This -/// test is the toolchain-free regression oracle for that bug: it generates -/// all targets with `[global] c_prefix = "myffi"` and asserts each one emits -/// the prefixed user symbol `myffi_calculator_add` and never the -/// default-prefixed `weaveffi_calculator_add`. -#[test] -fn custom_c_prefix_propagates_to_all_targets() { - let repo_root = repo_root(); - let input = repo_root.join("samples/calculator/calculator.yml"); - - let dir = tempfile::tempdir().expect("failed to create temp dir"); - let out = dir.path().join("out"); - let cfg = dir.path().join("cfg.toml"); - fs::write(&cfg, "[global]\nc_prefix = \"myffi\"\n").expect("failed to write cfg.toml"); - - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "generate", - input.to_str().unwrap(), - "-o", - out.to_str().unwrap(), - "--config", - cfg.to_str().unwrap(), - ]) - .assert() - .success(); - - // Every backend's output directory. The C symbol `calculator_add` is a - // plain sync function present in the calculator sample, so every backend - // that binds to it must reference the prefixed form. - for target in [ - "c", "cpp", "swift", "android", "node", "wasm", "python", "dotnet", "dart", "go", "ruby", - ] { - let target_dir = out.join(target); - assert!( - target_dir.is_dir(), - "target `{target}` produced no output directory at {}", - target_dir.display() - ); - let tree = read_tree(&target_dir); - assert!( - tree.contains("myffi_calculator_add"), - "target `{target}` did not honor the custom c_prefix \ - (missing user symbol `myffi_calculator_add`)" - ); - assert!( - !tree.contains("weaveffi_calculator_add"), - "target `{target}` leaked the default-prefixed user symbol \ - `weaveffi_calculator_add` despite a custom c_prefix" - ); - } -} - -#[test] -fn custom_c_prefix_propagates() { - let repo_root = repo_root(); - let input = repo_root.join("samples/calculator/calculator.yml"); - - let dir = tempfile::tempdir().expect("failed to create temp dir"); - let out = dir.path().join("out"); - let cfg = dir.path().join("cfg.toml"); - fs::write(&cfg, "[c]\nprefix = \"myffi\"\n").expect("failed to write cfg.toml"); - - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "generate", - input.to_str().unwrap(), - "-o", - out.to_str().unwrap(), - "--target", - "c,cpp", - "--config", - cfg.to_str().unwrap(), - ]) - .assert() - .success(); - - let header_path = out.join("c/myffi.h"); - assert!( - header_path.exists(), - "expected c/myffi.h to exist at {}", - header_path.display() - ); - let header = fs::read_to_string(&header_path).expect("missing c/myffi.h"); - - assert!( - header.contains("#define myffi_error weaveffi_error"), - "header missing #define myffi_error alias:\n{header}" - ); - assert!( - header.contains("#define myffi_handle_t weaveffi_handle_t"), - "header missing #define myffi_handle_t alias:\n{header}" - ); - assert!( - header.contains("#define myffi_free_string weaveffi_free_string"), - "header missing #define myffi_free_string alias:\n{header}" - ); - assert!( - header.contains("#define myffi_cancel_token_create weaveffi_cancel_token_create"), - "header missing #define myffi_cancel_token_create alias:\n{header}" - ); - - assert!( - header.contains("myffi_calculator_add"), - "header should declare prefixed user fn myffi_calculator_add:\n{header}" - ); - assert!( - !header.contains("weaveffi_calculator_add"), - "header should not contain default-prefixed user fn:\n{header}" - ); - - let cpp_header_path = out.join("cpp/weaveffi.hpp"); - let cpp = fs::read_to_string(&cpp_header_path).expect("missing cpp/weaveffi.hpp"); - assert!( - cpp.contains("extern \"C\" {"), - "C++ header missing extern \"C\" block:\n{cpp}" - ); - assert!( - cpp.contains("myffi_calculator_add"), - "C++ extern \"C\" block should reference prefixed C symbol myffi_calculator_add:\n{cpp}" - ); - assert!( - !cpp.contains("weaveffi_calculator_add"), - "C++ extern \"C\" block should not retain default-prefixed user fn:\n{cpp}" - ); - assert!( - cpp.contains("#define myffi_error weaveffi_error"), - "C++ header should also alias runtime symbols to the custom prefix:\n{cpp}" - ); -} diff --git a/crates/weaveffi-cli/tests/cli_check.rs b/crates/weaveffi-cli/tests/cli_check.rs index 1a360830..ec5f4ed2 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|lint --format json`. Each test runs the binary as a +//! `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. @@ -22,7 +22,7 @@ fn calculator_idl() -> std::path::PathBuf { .unwrap() .parent() .unwrap() - .join("samples/calculator/calculator.yml") + .join("samples/calculator/src/lib.rs") } #[test] @@ -76,7 +76,7 @@ fn diff_check_fails_when_idl_changed() { write_file( &idl, concat!( - "version: \"0.7.0\"\n", + "version: \"0.8.0\"\n", "modules:\n", " - name: calc\n", " functions:\n", @@ -102,7 +102,7 @@ fn diff_check_fails_when_idl_changed() { write_file( &idl, concat!( - "version: \"0.7.0\"\n", + "version: \"0.8.0\"\n", "modules:\n", " - name: calc\n", " functions:\n", @@ -183,13 +183,13 @@ fn validate_json_format_outputs_object() { } #[test] -fn lint_json_format_outputs_warnings_array() { +fn validate_warn_json_format_outputs_warnings_array() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("nodocs.yml"); write_file( &path, concat!( - "version: \"0.7.0\"\n", + "version: \"0.8.0\"\n", "modules:\n", " - name: nodocs\n", " functions:\n", @@ -200,14 +200,15 @@ fn lint_json_format_outputs_warnings_array() { let output = cargo_bin() .args([ - "lint", + "validate", path.to_str().unwrap(), + "--warn", "--format", "json", "--quiet", ]) .output() - .expect("failed to run weaveffi lint --format json"); + .expect("failed to run weaveffi validate --warn --format json"); let stdout = String::from_utf8_lossy(&output.stdout); let parsed: serde_json::Value = @@ -230,7 +231,8 @@ fn lint_json_format_outputs_warnings_array() { assert_eq!( parsed["ok"], - serde_json::Value::Bool(false), - "ok should be false when warnings are present" + serde_json::Value::Bool(true), + "warnings are advisory, so a valid IDL still reports ok" ); + assert!(output.status.success(), "warnings must not fail validate"); } diff --git a/crates/weaveffi-cli/tests/cli_contacts.rs b/crates/weaveffi-cli/tests/cli_contacts.rs deleted file mode 100644 index 65e2d571..00000000 --- a/crates/weaveffi-cli/tests/cli_contacts.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::path::Path; - -#[test] -fn generate_contacts_produces_all_targets() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let input = repo_root.join("samples/contacts/contacts.yml"); - - 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") - .args([ - "generate", - input.to_str().unwrap(), - "-o", - out_path.to_str().unwrap(), - ]) - .assert() - .success(); - - let header = - std::fs::read_to_string(out_path.join("c/weaveffi.h")).expect("missing c/weaveffi.h"); - assert!( - header.contains("weaveffi_contacts_ContactBook_new"), - "c/weaveffi.h should contain weaveffi_contacts_ContactBook_new" - ); - - assert!( - out_path.join("swift/Package.swift").exists(), - "missing swift/Package.swift" - ); - assert!( - out_path.join("android/build.gradle").exists(), - "missing android/build.gradle" - ); - - let types_dts = - std::fs::read_to_string(out_path.join("node/types.d.ts")).expect("missing node/types.d.ts"); - assert!( - types_dts.contains("interface Contact"), - "node/types.d.ts should contain interface Contact" - ); - - assert!( - out_path.join("wasm/README.md").exists(), - "missing wasm/README.md" - ); -} diff --git a/crates/weaveffi-cli/tests/cli_cpp.rs b/crates/weaveffi-cli/tests/cli_cpp.rs deleted file mode 100644 index 46f987fc..00000000 --- a/crates/weaveffi-cli/tests/cli_cpp.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::path::Path; - -#[test] -fn generate_cpp_contacts() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let input = repo_root.join("samples/contacts/contacts.yml"); - - 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") - .args([ - "generate", - input.to_str().unwrap(), - "-o", - out_path.to_str().unwrap(), - "--target", - "cpp", - ]) - .assert() - .success(); - - let hpp = std::fs::read_to_string(out_path.join("cpp/weaveffi.hpp")) - .expect("missing cpp/weaveffi.hpp"); - assert!( - hpp.contains("namespace weaveffi"), - "weaveffi.hpp should contain namespace weaveffi" - ); - - // Typed error surface: one domain exception per declaring module plus a - // subclass per code, and a domain check helper used by throwing wrappers. - assert!( - hpp.contains("class WeaveFFIError : public std::runtime_error"), - "missing generic exception" - ); - assert!( - hpp.contains("class ContactsError : public WeaveFFIError"), - "missing domain exception" - ); - assert!( - hpp.contains("class NotFoundError : public ContactsError"), - "missing per-code exception subclass" - ); - assert!( - hpp.contains("inline void check_contacts(weaveffi_error& err)"), - "missing per-domain check helper" - ); - - // Interface: RAII class with the canonical constructor mapped from `new`, - // methods on the wrapped handle, and the destroy symbol in the destructor. - assert!( - hpp.contains("class ContactBook {"), - "missing interface class" - ); - assert!( - hpp.contains("weaveffi_contacts_ContactBook_destroy"), - "interface destructor must call the destroy symbol" - ); - assert!( - hpp.contains("detail::check_contacts(err);"), - "throwing methods must use the domain check helper" - ); - - assert!( - out_path.join("cpp/CMakeLists.txt").exists(), - "missing cpp/CMakeLists.txt" - ); -} diff --git a/crates/weaveffi-cli/tests/cli_dart.rs b/crates/weaveffi-cli/tests/cli_dart.rs deleted file mode 100644 index cf8035ed..00000000 --- a/crates/weaveffi-cli/tests/cli_dart.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::path::Path; - -#[test] -fn generate_dart_contacts() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let input = repo_root.join("samples/contacts/contacts.yml"); - - 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") - .args([ - "generate", - input.to_str().unwrap(), - "-o", - out_path.to_str().unwrap(), - "--target", - "dart", - ]) - .assert() - .success(); - - let dart = std::fs::read_to_string(out_path.join("dart/lib/weaveffi.dart")) - .expect("missing dart/lib/weaveffi.dart"); - assert!( - dart.contains("import 'dart:ffi'"), - "weaveffi.dart should import dart:ffi" - ); - assert!( - dart.contains("enum ContactType {"), - "weaveffi.dart should contain ContactType enum" - ); - assert!( - dart.contains("class Contact {"), - "weaveffi.dart should contain Contact class" - ); - assert!( - dart.contains("class ContactBook {") && dart.contains("factory ContactBook() {"), - "weaveffi.dart should contain the ContactBook interface class" - ); - assert!( - dart.contains("class ContactsException extends WeaveFFIException {"), - "weaveffi.dart should contain the ContactsException domain exception" - ); - assert!( - dart.contains("weaveffi_contacts_ContactBook_add"), - "weaveffi.dart should contain the ContactBook add member symbol" - ); - - assert!( - out_path.join("dart/pubspec.yaml").exists(), - "missing dart/pubspec.yaml" - ); - assert!( - out_path.join("dart/README.md").exists(), - "missing dart/README.md" - ); -} diff --git a/crates/weaveffi-cli/tests/cli_diagnostics.rs b/crates/weaveffi-cli/tests/cli_diagnostics.rs index 66c54b36..0c6ed7f3 100644 --- a/crates/weaveffi-cli/tests/cli_diagnostics.rs +++ b/crates/weaveffi-cli/tests/cli_diagnostics.rs @@ -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.7.0\"\nmodules:\n - name: [bad\n"; + let yaml = "version: \"0.8.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.7.0" + let yaml = r#"version: "0.8.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.7.0" + let yaml = r#"version: "0.8.0" modules: - name: alpha functions: diff --git a/crates/weaveffi-cli/tests/cli_diff.rs b/crates/weaveffi-cli/tests/cli_diff.rs index aab6b7c1..dd527398 100644 --- a/crates/weaveffi-cli/tests/cli_diff.rs +++ b/crates/weaveffi-cli/tests/cli_diff.rs @@ -6,7 +6,7 @@ use predicates::prelude::*; fn diff_against_empty_dir() { 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/calculator.yml"); + let input = repo_root.join("samples/calculator/src/lib.rs"); let tmp = tempfile::tempdir().expect("failed to create temp dir"); let empty_out = tmp.path().join("empty"); @@ -39,7 +39,7 @@ fn diff_against_empty_dir() { 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/calculator.yml"); + let input = repo_root.join("samples/calculator/src/lib.rs"); let tmp = tempfile::tempdir().expect("failed to create temp dir"); let out_path = tmp.path().join("generated"); @@ -76,7 +76,7 @@ fn diff_no_changes() { 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/kvstore.yml"); + let input = repo_root.join("samples/kvstore/src/lib.rs"); let tmp = tempfile::tempdir().expect("failed to create temp dir"); let out_path = tmp.path().join("generated"); diff --git a/crates/weaveffi-cli/tests/cli_doctor.rs b/crates/weaveffi-cli/tests/cli_doctor.rs deleted file mode 100644 index bd4317a4..00000000 --- a/crates/weaveffi-cli/tests/cli_doctor.rs +++ /dev/null @@ -1,238 +0,0 @@ -//! Integration tests for the `weaveffi doctor` subcommand. These exercise -//! both the human-readable and `--format json` outputs as well as the -//! `--target` filter that lets users (and CI) ask about a single language -//! toolchain at a time. - -fn cargo_bin() -> assert_cmd::Command { - assert_cmd::Command::cargo_bin("weaveffi").expect("binary not found") -} - -/// `weaveffi doctor --format json` should write a syntactically valid JSON -/// array to stdout, with each element shaped like the documented -/// `DoctorCheck` struct (`id`, `name`, `ok`, `version`, `hint`, -/// `applies_to`). The required `rustc` and `cargo` checks must always be -/// present, and every `applies_to` entry must be a string. -#[test] -fn doctor_json_outputs_valid_json() { - let output = cargo_bin() - .args(["doctor", "--format", "json"]) - .output() - .expect("failed to run weaveffi doctor --format json"); - - let stdout = String::from_utf8_lossy(&output.stdout); - let value: serde_json::Value = serde_json::from_str(&stdout) - .unwrap_or_else(|e| panic!("doctor --format json produced invalid JSON: {e}\n{stdout}")); - - let arr = value - .as_array() - .expect("doctor --format json should produce a top-level array"); - assert!(!arr.is_empty(), "doctor checks should not be empty"); - - let mut ids = Vec::new(); - for check in arr { - let obj = check - .as_object() - .expect("each check should be a JSON object"); - - for field in ["id", "name", "ok", "version", "hint", "applies_to"] { - assert!( - obj.contains_key(field), - "check is missing field '{field}': {check}" - ); - } - - assert!(obj["id"].is_string(), "id should be a string: {check}"); - assert!(obj["name"].is_string(), "name should be a string: {check}"); - assert!(obj["ok"].is_boolean(), "ok should be a bool: {check}"); - - let applies_to = obj["applies_to"] - .as_array() - .expect("applies_to should be an array"); - for entry in applies_to { - assert!( - entry.is_string(), - "applies_to entries should be strings: {check}" - ); - } - - ids.push(obj["id"].as_str().unwrap().to_string()); - } - - for required in ["rustc", "cargo"] { - assert!( - ids.iter().any(|id| id == required), - "doctor JSON output should always include '{required}' check, got ids: {ids:?}" - ); - } -} - -/// The default human-readable output should mention every language -/// toolchain WeaveFFI generates code for so users can scan it and see -/// what's installed. We also re-assert the existing section markers so a -/// future refactor can't silently drop them. -#[test] -fn doctor_human_readable_lists_all_targets() { - let output = cargo_bin() - .arg("doctor") - .output() - .expect("failed to run weaveffi doctor"); - - assert!( - output.status.success(), - "doctor (no --target) should always exit 0, got {}: {}", - output.status, - String::from_utf8_lossy(&output.stderr) - ); - - let stdout = String::from_utf8_lossy(&output.stdout); - - let expected_sections = [ - "Required toolchain:", - "Node.js:", - "Cross-compilation targets:", - "WebAssembly tools:", - "C++:", - "Dart:", - "Go:", - "Ruby:", - ".NET:", - "Python:", - ]; - for section in expected_sections { - assert!( - stdout.contains(section), - "doctor human output should contain section header '{section}', got:\n{stdout}" - ); - } - - let expected_labels = [ - "Rust compiler", - "CMake", - "Dart SDK", - "Flutter (optional)", - "Go", - "Ruby", - "RubyGems", - "Bundler", - ".NET SDK", - "Python 3", - "Python ctypes module", - ]; - for label in expected_labels { - assert!( - stdout.contains(label), - "doctor human output should mention '{label}', got:\n{stdout}" - ); - } -} - -/// `--target dart` should restrict the check list to checks whose -/// `applies_to` contains either `dart` or the wildcard `*` (which marks -/// the always-run Rust toolchain). The result must be strictly smaller -/// than the unfiltered list and must never include unrelated checks like -/// the Node or Ruby ones. -#[test] -fn doctor_target_filter_runs_subset() { - let full = cargo_bin() - .args(["doctor", "--format", "json"]) - .output() - .expect("failed to run unfiltered doctor"); - let full_stdout = String::from_utf8_lossy(&full.stdout); - let full_value: serde_json::Value = - serde_json::from_str(&full_stdout).expect("full doctor JSON should parse"); - let full_len = full_value.as_array().unwrap().len(); - - let filtered = cargo_bin() - .args(["doctor", "--target", "dart", "--format", "json"]) - .output() - .expect("failed to run --target dart doctor"); - let filtered_stdout = String::from_utf8_lossy(&filtered.stdout); - let filtered_value: serde_json::Value = - serde_json::from_str(&filtered_stdout).expect("filtered doctor JSON should parse"); - let filtered_arr = filtered_value - .as_array() - .expect("filtered output should be an array"); - - assert!( - filtered_arr.len() < full_len, - "filtered output ({}) should have fewer checks than full output ({})", - filtered_arr.len(), - full_len - ); - - for check in filtered_arr { - let applies_to: Vec<&str> = check["applies_to"] - .as_array() - .unwrap() - .iter() - .filter_map(|v| v.as_str()) - .collect(); - assert!( - applies_to.contains(&"dart") || applies_to.contains(&"*"), - "check {check} should apply to dart or be a wildcard" - ); - } - - let ids: Vec<&str> = filtered_arr - .iter() - .filter_map(|c| c["id"].as_str()) - .collect(); - for unrelated in ["node", "npm", "ruby", "gem", "dotnet", "go"] { - assert!( - !ids.contains(&unrelated), - "--target dart should not include '{unrelated}', got ids: {ids:?}" - ); - } - assert!( - ids.contains(&"dart"), - "--target dart should include the dart check, got ids: {ids:?}" - ); - assert!( - ids.contains(&"rustc") && ids.contains(&"cargo"), - "--target dart should include the always-run rustc/cargo checks, got ids: {ids:?}" - ); -} - -/// `--target package` is the pseudo-target for `weaveffi package --build`: it -/// should surface a producer cross-target check for every platform in the v1 -/// matrix (plus the always-run rustc/cargo checks) and nothing language -/// specific. -#[test] -fn doctor_target_package_lists_producer_targets() { - let output = cargo_bin() - .args(["doctor", "--target", "package", "--format", "json"]) - .output() - .expect("failed to run --target package doctor"); - let stdout = String::from_utf8_lossy(&output.stdout); - let value: serde_json::Value = - serde_json::from_str(&stdout).expect("package doctor JSON should parse"); - let ids: Vec<&str> = value - .as_array() - .expect("array") - .iter() - .filter_map(|c| c["id"].as_str()) - .collect(); - - for id in [ - "pkg_target_darwin_arm64", - "pkg_target_darwin_x64", - "pkg_target_linux_x64", - "pkg_target_linux_arm64", - "pkg_target_windows_x64", - ] { - assert!( - ids.contains(&id), - "--target package should include '{id}', got ids: {ids:?}" - ); - } - assert!( - ids.contains(&"rustc") && ids.contains(&"cargo"), - "--target package should include the always-run rustc/cargo checks, got ids: {ids:?}" - ); - for unrelated in ["node", "dart", "ruby", "dotnet", "go", "target_wasm"] { - assert!( - !ids.contains(&unrelated), - "--target package should not include '{unrelated}', got ids: {ids:?}" - ); - } -} diff --git a/crates/weaveffi-cli/tests/cli_dotnet.rs b/crates/weaveffi-cli/tests/cli_dotnet.rs deleted file mode 100644 index 1b5b166b..00000000 --- a/crates/weaveffi-cli/tests/cli_dotnet.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::path::Path; - -#[test] -fn generate_dotnet_contacts() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let input = repo_root.join("samples/contacts/contacts.yml"); - - 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") - .args([ - "generate", - input.to_str().unwrap(), - "-o", - out_path.to_str().unwrap(), - "--target", - "dotnet", - ]) - .assert() - .success(); - - let cs = std::fs::read_to_string(out_path.join("dotnet/WeaveFFI.cs")) - .expect("missing dotnet/WeaveFFI.cs"); - assert!( - cs.contains("DllImport"), - "WeaveFFI.cs should contain DllImport" - ); - - // Interface: opaque-handle class with a real ctor, PascalCase methods, - // camelCase parameters, and Dispose lowering to the destroy symbol. - assert!( - cs.contains("public class ContactBook : IDisposable"), - "WeaveFFI.cs should contain the ContactBook interface class" - ); - assert!( - cs.contains("public ContactBook()"), - "WeaveFFI.cs should map the `new` constructor to a real C# constructor" - ); - assert!( - cs.contains( - "public Contact Add(string firstName, string lastName, string? email, \ - ContactType contactType)" - ), - "WeaveFFI.cs should contain the PascalCase Add method with camelCase parameters" - ); - assert!( - cs.contains("public Contact Get(long id)"), - "WeaveFFI.cs should contain the Get method" - ); - assert!( - cs.contains("NativeMethods.weaveffi_contacts_ContactBook_destroy(_handle);"), - "WeaveFFI.cs should dispose through the interface destroy symbol" - ); - - // Typed errors: domain exception, code constants, per-domain check. - assert!( - cs.contains("public class ContactsException : WeaveFFIException"), - "WeaveFFI.cs should contain the ContactsException domain exception" - ); - assert!( - cs.contains("public const int InvalidName = 1;") - && cs.contains("public const int NotFound = 2;"), - "WeaveFFI.cs should surface the domain codes as constants" - ); - assert!( - cs.contains("WeaveFFIError.CheckContacts(err);"), - "throwing wrappers should report through the typed check helper" - ); - - assert!( - out_path.join("dotnet/WeaveFFI.csproj").exists(), - "missing dotnet/WeaveFFI.csproj" - ); -} diff --git a/crates/weaveffi-cli/tests/cli_errors.rs b/crates/weaveffi-cli/tests/cli_errors.rs index 77807f8b..d7589ede 100644 --- a/crates/weaveffi-cli/tests/cli_errors.rs +++ b/crates/weaveffi-cli/tests/cli_errors.rs @@ -85,7 +85,7 @@ fn parse_error_toml_shows_filename() { fn validation_error_duplicate_module_shows_suggestion() { let dir = tempfile::tempdir().unwrap(); let yaml = r#" -version: "0.7.0" +version: "0.8.0" modules: - name: foo functions: @@ -123,7 +123,7 @@ modules: fn async_function_validates_successfully() { let dir = tempfile::tempdir().unwrap(); let yaml = r#" -version: "0.7.0" +version: "0.8.0" modules: - name: mymod functions: @@ -149,7 +149,7 @@ modules: fn validation_error_duplicate_function_shows_suggestion() { let dir = tempfile::tempdir().unwrap(); let yaml = r#" -version: "0.7.0" +version: "0.8.0" modules: - name: mymod functions: @@ -209,7 +209,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.7.0" +version: "0.8.0" modules: - name: dup functions: @@ -249,7 +249,7 @@ modules: fn validate_with_warnings() { let dir = tempfile::tempdir().unwrap(); let yaml = r#" -version: "0.7.0" +version: "0.8.0" modules: - name: nodocs functions: @@ -274,7 +274,7 @@ modules: "expected module name in warning, got: {stderr}" ); assert!( - stderr.contains("no doc comments"), + stderr.contains("has no doc comment"), "expected doc comment warning text, got: {stderr}" ); } diff --git a/crates/weaveffi-cli/tests/cli_format.rs b/crates/weaveffi-cli/tests/cli_format.rs deleted file mode 100644 index 4125d278..00000000 --- a/crates/weaveffi-cli/tests/cli_format.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::io::Write; - -fn cargo_bin() -> assert_cmd::Command { - assert_cmd::Command::cargo_bin("weaveffi").expect("binary not found") -} - -fn write(path: &std::path::Path, contents: &str) { - let mut f = std::fs::File::create(path).unwrap(); - f.write_all(contents.as_bytes()).unwrap(); -} - -#[test] -fn format_check_passes_for_canonical_input() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("api.yml"); - write( - &path, - concat!( - "version: \"0.7.0\"\n", - "modules:\n", - " - name: math\n", - " functions:\n", - " - name: add\n", - " params: []\n", - ), - ); - - cargo_bin() - .args(["format", path.to_str().unwrap()]) - .assert() - .success(); - - cargo_bin() - .args(["format", path.to_str().unwrap(), "--check"]) - .assert() - .success(); -} - -#[test] -fn format_rewrites_unsorted_keys() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("api.yml"); - write( - &path, - concat!( - "modules:\n", - " - functions:\n", - " - return: i32\n", - " params:\n", - " - { type: i32, name: a }\n", - " - { type: i32, name: b }\n", - " name: add\n", - " name: math\n", - "version: \"0.7.0\"\n", - ), - ); - let original = std::fs::read_to_string(&path).unwrap(); - - cargo_bin() - .args(["format", path.to_str().unwrap()]) - .assert() - .success(); - - let formatted = std::fs::read_to_string(&path).unwrap(); - assert_ne!(formatted, original, "format should rewrite the file"); - - let key_lines: Vec<&str> = formatted - .lines() - .filter(|l| { - l.trim_start() - .starts_with(|c: char| c.is_alphabetic() || c == '"') - && l.contains(':') - }) - .collect(); - assert!( - key_lines.iter().any(|l| l.starts_with("modules:")), - "expected top-level 'modules:' key, got: {key_lines:?}" - ); - let modules_idx = key_lines - .iter() - .position(|l| l.starts_with("modules:")) - .unwrap(); - let version_idx = key_lines - .iter() - .position(|l| l.starts_with("version:")) - .unwrap(); - assert!( - modules_idx < version_idx, - "modules should be sorted before version at top level: {key_lines:?}" - ); - - cargo_bin() - .args(["format", path.to_str().unwrap(), "--check"]) - .assert() - .success(); -} - -#[test] -fn format_check_fails_for_unsorted_input() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("api.yml"); - write( - &path, - concat!( - "modules:\n", - " - functions:\n", - " - return: i32\n", - " params: []\n", - " name: noop\n", - " name: math\n", - "version: \"0.7.0\"\n", - ), - ); - let original = std::fs::read_to_string(&path).unwrap(); - - let output = cargo_bin() - .args(["format", path.to_str().unwrap(), "--check"]) - .output() - .unwrap(); - assert!( - !output.status.success(), - "--check should exit non-zero for unsorted input: stdout={}, stderr={}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - - let after = std::fs::read_to_string(&path).unwrap(); - assert_eq!(after, original, "--check must not modify the input file"); -} diff --git a/crates/weaveffi-cli/tests/cli_formats.rs b/crates/weaveffi-cli/tests/cli_formats.rs index 13a65342..18e2fb71 100644 --- a/crates/weaveffi-cli/tests/cli_formats.rs +++ b/crates/weaveffi-cli/tests/cli_formats.rs @@ -8,9 +8,9 @@ fn repo_root() -> &'static Path { } fn load_calculator_api() -> Api { - let yml = std::fs::read_to_string(repo_root().join("samples/calculator/calculator.yml")) - .expect("failed to read calculator.yml"); - serde_yaml::from_str(&yml).expect("failed to parse calculator.yml") + let src = std::fs::read_to_string(repo_root().join("samples/calculator/src/lib.rs")) + .expect("failed to read calculator lib.rs"); + weaveffi_bridge::api_from_src_stringly(&src).expect("failed to extract calculator API") } #[test] diff --git a/crates/weaveffi-cli/tests/cli_generate.rs b/crates/weaveffi-cli/tests/cli_generate.rs index 6d0ce8b1..8f6bfeee 100644 --- a/crates/weaveffi-cli/tests/cli_generate.rs +++ b/crates/weaveffi-cli/tests/cli_generate.rs @@ -5,7 +5,7 @@ use std::path::Path; fn generate_produces_expected_files() { 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/calculator.yml"); + let input = repo_root.join("samples/calculator/src/lib.rs"); let out_dir = tempfile::tempdir().expect("failed to create temp dir"); let out_path = out_dir.path(); @@ -47,7 +47,7 @@ fn generate_produces_expected_files() { fn generate_with_target_filter() { 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/calculator.yml"); + let input = repo_root.join("samples/calculator/src/lib.rs"); let out_dir = tempfile::tempdir().expect("failed to create temp dir"); let out_path = out_dir.path(); @@ -79,7 +79,7 @@ fn generate_with_target_filter() { fn generate_cpp_target_filter() { 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/calculator.yml"); + let input = repo_root.join("samples/calculator/src/lib.rs"); let out_dir = tempfile::tempdir().expect("failed to create temp dir"); let out_path = out_dir.path(); @@ -111,7 +111,7 @@ fn generate_cpp_target_filter() { fn validate_command_succeeds() { 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/calculator.yml"); + let input = repo_root.join("samples/calculator/src/lib.rs"); assert_cmd::Command::cargo_bin("weaveffi") .expect("binary not found") @@ -125,7 +125,7 @@ fn validate_command_succeeds() { fn quiet_flag_suppresses_output() { 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/calculator.yml"); + let input = repo_root.join("samples/calculator/src/lib.rs"); let out_dir = tempfile::tempdir().expect("failed to create temp dir"); let out_path = out_dir.path(); diff --git a/crates/weaveffi-cli/tests/cli_go.rs b/crates/weaveffi-cli/tests/cli_go.rs deleted file mode 100644 index 133296ca..00000000 --- a/crates/weaveffi-cli/tests/cli_go.rs +++ /dev/null @@ -1,92 +0,0 @@ -use std::path::Path; - -#[test] -fn generate_go_contacts() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let input = repo_root.join("samples/contacts/contacts.yml"); - - 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") - .args([ - "generate", - input.to_str().unwrap(), - "-o", - out_path.to_str().unwrap(), - "--target", - "go", - ]) - .assert() - .success(); - - let go = - std::fs::read_to_string(out_path.join("go/weaveffi.go")).expect("missing go/weaveffi.go"); - // The Go package clause follows the resolved package identity (contacts.yml - // declares `package.name: contacts`), not the `weaveffi` brand. - assert!( - go.contains("package contacts"), - "weaveffi.go should contain identity-derived package declaration" - ); - assert!( - go.contains("type ContactType int32"), - "weaveffi.go should contain ContactType enum" - ); - assert!( - go.contains("type Contact struct {"), - "weaveffi.go should contain Contact struct" - ); - - // The ContactBook interface surfaces as a wrapper struct with a factory - // constructor, methods on the wrapper, and an explicit Close. - assert!( - go.contains("type ContactBook struct {"), - "weaveffi.go should contain the ContactBook wrapper" - ); - assert!( - go.contains("func NewContactBook() *ContactBook {"), - "ctor named `new` should surface as NewContactBook" - ); - assert!( - go.contains("func (s *ContactBook) Add(firstName string, lastName string, email *string, contactType ContactType) (Contact, error) {"), - "throwing method should keep the (T, error) shape with a by-value record" - ); - assert!( - go.contains("func (s *ContactBook) Count() int32 {"), - "plain method should have a bare return" - ); - assert!( - go.contains("C.weaveffi_contacts_ContactBook_add(s.ptr, "), - "methods should pass s.ptr as the leading C argument" - ); - assert!( - go.contains("C.weaveffi_contacts_ContactBook_destroy(s.ptr)"), - "Close should call the interface destroy symbol" - ); - - // The typed error domain: one Go error type plus exported code constants. - assert!( - go.contains("type ContactsError struct {"), - "weaveffi.go should contain the typed ContactsError" - ); - assert!( - go.contains("ContactsErrorInvalidName int32 = 1"), - "missing InvalidName code constant" - ); - assert!( - go.contains("ContactsErrorNotFound int32 = 2"), - "missing NotFound code constant" - ); - assert!( - go.contains("wvMapContacts(wvTakeError(&cErr))"), - "throwing methods should map through the domain helper" - ); - - assert!(out_path.join("go/go.mod").exists(), "missing go/go.mod"); - assert!( - out_path.join("go/README.md").exists(), - "missing go/README.md" - ); -} diff --git a/crates/weaveffi-cli/tests/cli_inline_generators.rs b/crates/weaveffi-cli/tests/cli_inline_generators.rs deleted file mode 100644 index a6fbed95..00000000 --- a/crates/weaveffi-cli/tests/cli_inline_generators.rs +++ /dev/null @@ -1,183 +0,0 @@ -use std::fs; - -#[test] -fn inline_dart_package_name_used() { - let dir = tempfile::tempdir().expect("failed to create temp dir"); - let yml = dir.path().join("api.yml"); - fs::write( - &yml, - concat!( - "version: \"0.7.0\"\n", - "modules:\n", - " - name: math\n", - " functions:\n", - " - name: add\n", - " params:\n", - " - { name: a, type: i32 }\n", - " - { name: b, type: i32 }\n", - " return: i32\n", - "generators:\n", - " dart:\n", - " package_name: my_inline_dart_pkg\n", - ), - ) - .expect("failed to write api.yml"); - - let out = dir.path().join("out"); - - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "generate", - yml.to_str().unwrap(), - "-o", - out.to_str().unwrap(), - "--target", - "dart", - ]) - .assert() - .success(); - - let pubspec = - fs::read_to_string(out.join("dart/pubspec.yaml")).expect("missing dart/pubspec.yaml"); - assert!( - pubspec.contains("name: my_inline_dart_pkg"), - "pubspec should pick up inline dart.package_name override; got:\n{pubspec}" - ); -} - -const LISTENER_IDL: &str = concat!( - "version: \"0.7.0\"\n", - "modules:\n", - " - name: events\n", - " callbacks:\n", - " - name: OnMessage\n", - " params:\n", - " - { name: message, type: string }\n", - " listeners:\n", - " - name: message_listener\n", - " event_callback: OnMessage\n", - " functions:\n", - " - name: send\n", - " params:\n", - " - { name: text, type: string }\n", -); - -/// The wasm target supports listeners in its standard loader: a -/// listener-bearing IDL generates real register/unregister bindings (no -/// opt-in flag, no capability warning, no throwing stubs). -#[test] -fn wasm_generates_listener_bindings() { - let dir = tempfile::tempdir().expect("failed to create temp dir"); - let yml = dir.path().join("api.yml"); - fs::write(&yml, LISTENER_IDL).expect("failed to write api.yml"); - let out = dir.path().join("out"); - - let assert = assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "generate", - yml.to_str().unwrap(), - "-o", - out.to_str().unwrap(), - "--target", - "wasm", - ]) - .assert() - .success(); - - let stderr = String::from_utf8_lossy(&assert.get_output().stderr).to_string(); - assert!( - !stderr.contains("does not support"), - "no capability warning should be printed:\n{stderr}" - ); - - let js = fs::read_to_string(out.join("wasm/weaveffi_wasm.js")) - .expect("missing wasm/weaveffi_wasm.js"); - assert!( - js.contains("registerMessageListener(callback) {"), - "register should take the JS callback:\n{js}" - ); - assert!( - js.contains("wasm.weaveffi_events_register_message_listener("), - "register should call the producer symbol:\n{js}" - ); - assert!( - !js.contains("is not supported by the wasm target"), - "no throwing stubs in the standard loader:\n{js}" - ); - - let dts = fs::read_to_string(out.join("wasm/weaveffi_wasm.d.ts")) - .expect("missing wasm/weaveffi_wasm.d.ts"); - assert!( - dts.contains("registerMessageListener(callback: (message: string) => void): number;"), - "d.ts should declare the register entry point:\n{dts}" - ); - assert!( - dts.contains("unregisterMessageListener(id: number): void;"), - "d.ts should declare the unregister entry point:\n{dts}" - ); -} - -/// `generators.wasm.emscripten: true` swaps the fetch-based loader for one -/// that accepts a pre-initialized Emscripten module and binds its -/// underscore-prefixed exports to the symbol names the glue calls. -#[test] -fn wasm_emscripten_mode_generates_module_loader() { - let dir = tempfile::tempdir().expect("failed to create temp dir"); - let yml = dir.path().join("api.yml"); - fs::write( - &yml, - concat!( - "version: \"0.7.0\"\n", - "modules:\n", - " - name: math\n", - " functions:\n", - " - name: add\n", - " params:\n", - " - { name: a, type: i32 }\n", - " - { name: b, type: i32 }\n", - " return: i32\n", - "generators:\n", - " wasm:\n", - " emscripten: true\n", - ), - ) - .expect("failed to write api.yml"); - let out = dir.path().join("out"); - - assert_cmd::Command::cargo_bin("weaveffi") - .expect("binary not found") - .args([ - "generate", - yml.to_str().unwrap(), - "-o", - out.to_str().unwrap(), - "--target", - "wasm", - ]) - .assert() - .success(); - - let js = fs::read_to_string(out.join("wasm/weaveffi_wasm.js")) - .expect("missing wasm/weaveffi_wasm.js"); - assert!( - js.contains("export async function loadWeaveffiWasm(module) {"), - "loader should accept the Emscripten module:\n{js}" - ); - assert!( - js.contains("weaveffi_math_add: m['_weaveffi_math_add'],"), - "loader should bind the underscore-prefixed export:\n{js}" - ); - assert!( - !js.contains("WebAssembly.instantiate"), - "Emscripten mode must not instantiate the wasm itself:\n{js}" - ); - - let dts = fs::read_to_string(out.join("wasm/weaveffi_wasm.d.ts")) - .expect("missing wasm/weaveffi_wasm.d.ts"); - assert!( - dts.contains("loadWeaveffiWasm(module: object | Promise)"), - "d.ts should declare the module-taking loader:\n{dts}" - ); -} diff --git a/crates/weaveffi-cli/tests/cli_inventory.rs b/crates/weaveffi-cli/tests/cli_inventory.rs deleted file mode 100644 index 59c9a8c9..00000000 --- a/crates/weaveffi-cli/tests/cli_inventory.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::path::Path; - -#[test] -fn generate_inventory_all_targets() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let input = repo_root.join("samples/inventory/inventory.yml"); - - 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") - .args([ - "generate", - input.to_str().unwrap(), - "-o", - out_path.to_str().unwrap(), - ]) - .assert() - .success(); - - // All target directories exist - for dir in ["c", "swift", "android", "node", "wasm", "python", "dotnet"] { - assert!( - out_path.join(dir).exists(), - "missing target directory: {dir}" - ); - } - - // C header contains functions for both modules - let header = - std::fs::read_to_string(out_path.join("c/weaveffi.h")).expect("missing c/weaveffi.h"); - for prefix in ["weaveffi_products_", "weaveffi_orders_"] { - assert!( - header.contains(prefix), - "c/weaveffi.h should contain {prefix} prefixed functions" - ); - } - - // Node types.d.ts contains interfaces for all structs - let types_dts = - std::fs::read_to_string(out_path.join("node/types.d.ts")).expect("missing node/types.d.ts"); - for iface in [ - "interface Product", - "interface OrderItem", - "interface Order", - ] { - assert!( - types_dts.contains(iface), - "node/types.d.ts should contain {iface}" - ); - } - - // Python weaveffi.py contains classes from both modules - let weaveffi_py = std::fs::read_to_string(out_path.join("python/inventory/weaveffi.py")) - .expect("missing python/inventory/weaveffi.py"); - assert!( - weaveffi_py.contains("class Catalog:"), - "weaveffi.py should contain the products module's Catalog interface" - ); - assert!( - weaveffi_py.contains("class Order:"), - "weaveffi.py should contain the orders module's Order type" - ); - assert!( - weaveffi_py.contains("class ProductNotFound(ProductsError):"), - "weaveffi.py should contain typed products error classes" - ); -} diff --git a/crates/weaveffi-cli/tests/cli_kvstore.rs b/crates/weaveffi-cli/tests/cli_kvstore.rs deleted file mode 100644 index 739f53f5..00000000 --- a/crates/weaveffi-cli/tests/cli_kvstore.rs +++ /dev/null @@ -1,144 +0,0 @@ -use std::path::Path; - -#[test] -fn generate_kvstore_all_targets() { - 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/kvstore.yml"); - - 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") - .args([ - "generate", - input.to_str().unwrap(), - "-o", - out_path.to_str().unwrap(), - ]) - .assert() - .success(); - - for dir in [ - "c", "cpp", "swift", "android", "node", "wasm", "python", "dotnet", "dart", "go", "ruby", - ] { - assert!( - out_path.join(dir).exists(), - "missing target directory: {dir}" - ); - } - - let c_header = - std::fs::read_to_string(out_path.join("c/weaveffi.h")).expect("missing c/weaveffi.h"); - assert!( - c_header.contains("weaveffi_kv_Store_compact_async"), - "c/weaveffi.h should declare the cancellable async compact entry point" - ); - assert!( - c_header.contains("weaveffi_cancel_token* cancel_token"), - "c/weaveffi.h should pass a cancel_token to compact" - ); - assert!( - c_header.contains("typedef struct weaveffi_kv_Store_ListKeysIterator"), - "c/weaveffi.h should define the streaming iterator type" - ); - - let cpp = std::fs::read_to_string(out_path.join("cpp/weaveffi.hpp")) - .expect("missing cpp/weaveffi.hpp"); - assert!( - cpp.contains("std::future"), - "cpp/weaveffi.hpp should expose async functions as std::future" - ); - assert!( - cpp.contains("namespace kvstore"), - "cpp/weaveffi.hpp should pick up inline cpp.namespace override" - ); - - let swift_path = out_path.join("swift/Sources/Kvstore/Kvstore.swift"); - let swift = std::fs::read_to_string(&swift_path).unwrap_or_else(|e| { - panic!("missing {} ({e})", swift_path.display()); - }); - assert!( - swift.contains("async throws"), - "Kvstore.swift should mark compact_async as async throws" - ); - - let kotlin = - std::fs::read_to_string(out_path.join("android/src/main/kotlin/com/weaveffi/WeaveFFI.kt")) - .expect("missing android WeaveFFI.kt"); - assert!( - kotlin.contains("suspend"), - "WeaveFFI.kt should expose compact_async as a suspend fun" - ); - - let node_dts = - std::fs::read_to_string(out_path.join("node/types.d.ts")).expect("missing node/types.d.ts"); - assert!( - node_dts.contains("Promise<"), - "node/types.d.ts should return Promise for async functions" - ); - - let wasm_dts = std::fs::read_to_string(out_path.join("wasm/weaveffi_wasm.d.ts")) - .expect("missing wasm/weaveffi_wasm.d.ts"); - assert!( - wasm_dts.contains("Promise<"), - "wasm/weaveffi_wasm.d.ts should return Promise for async functions" - ); - - let python = std::fs::read_to_string(out_path.join("python/kvstore/weaveffi.py")) - .expect("missing python/kvstore/weaveffi.py"); - assert!( - python.contains("async def compact"), - "weaveffi.py should expose Store.compact as an async def" - ); - - let dotnet_path = out_path.join("dotnet/Kvstore.cs"); - let dotnet = std::fs::read_to_string(&dotnet_path).unwrap_or_else(|e| { - panic!("missing {} ({e})", dotnet_path.display()); - }); - assert!( - dotnet.contains("Task<"), - "Kvstore.cs should return a Task<...> for async functions" - ); - assert!( - dotnet.contains("namespace Kvstore"), - "Kvstore.cs should pick up inline dotnet.namespace override" - ); - - let dart = std::fs::read_to_string(out_path.join("dart/lib/weaveffi.dart")) - .expect("missing dart/lib/weaveffi.dart"); - assert!( - dart.contains("Future<"), - "dart/lib/weaveffi.dart should return Future for async functions" - ); - let pubspec = std::fs::read_to_string(out_path.join("dart/pubspec.yaml")) - .expect("missing dart/pubspec.yaml"); - assert!( - pubspec.contains("name: kvstore"), - "dart/pubspec.yaml should pick up inline dart.package_name override" - ); - - let go = - std::fs::read_to_string(out_path.join("go/weaveffi.go")).expect("missing go/weaveffi.go"); - assert!( - go.contains("func (s *Store) Put(") && go.contains("func GetStats("), - "go/weaveffi.go should expose Store methods and nested-module functions" - ); - let go_mod = std::fs::read_to_string(out_path.join("go/go.mod")).expect("missing go/go.mod"); - assert!( - go_mod.contains("github.com/example/kvstore"), - "go/go.mod should pick up inline go.module_path override" - ); - - let ruby = std::fs::read_to_string(out_path.join("ruby/lib/kvstore.rb")) - .expect("missing ruby/lib/kvstore.rb"); - assert!( - ruby.contains("module Kvstore"), - "ruby/lib/weaveffi.rb should pick up inline ruby.module_name override" - ); - assert!( - ruby.contains(":weaveffi_kv_Store_open") && ruby.contains(":weaveffi_kv_Store_legacy_put"), - "ruby/lib/weaveffi.rb should attach Store interface entry points including legacy_put" - ); -} diff --git a/crates/weaveffi-cli/tests/cli_lint.rs b/crates/weaveffi-cli/tests/cli_lint.rs deleted file mode 100644 index 325e7ad4..00000000 --- a/crates/weaveffi-cli/tests/cli_lint.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::io::Write; - -fn cargo_bin() -> assert_cmd::Command { - assert_cmd::Command::cargo_bin("weaveffi").expect("binary not found") -} - -#[test] -fn lint_clean_calculator() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let repo_root = std::path::Path::new(manifest_dir) - .parent() - .unwrap() - .parent() - .unwrap(); - let input = repo_root.join("samples/calculator/calculator.yml"); - - cargo_bin() - .args(["lint", input.to_str().unwrap()]) - .assert() - .success() - .stdout(predicates::prelude::predicate::str::contains( - "No warnings.", - )); -} - -#[test] -fn lint_warns_on_undocumented() { - let dir = tempfile::tempdir().unwrap(); - let yaml = r#" -version: "0.7.0" -modules: - - name: nodocs - functions: - - name: do_stuff - params: [] -"#; - let path = dir.path().join("nodocs.yml"); - let mut f = std::fs::File::create(&path).unwrap(); - f.write_all(yaml.as_bytes()).unwrap(); - - let output = cargo_bin() - .args(["lint", path.to_str().unwrap()]) - .output() - .unwrap(); - - assert!( - !output.status.success(), - "lint should fail when warnings are present" - ); - let stderr = String::from_utf8_lossy(&output.stderr); - assert!( - stderr.contains("warning:"), - "expected warning prefix in stderr, got: {stderr}" - ); - assert!( - stderr.contains("no doc comments"), - "expected doc comment warning text, got: {stderr}" - ); -} diff --git a/crates/weaveffi-cli/tests/cli_package.rs b/crates/weaveffi-cli/tests/cli_package.rs index 30d85429..f748a81a 100644 --- a/crates/weaveffi-cli/tests/cli_package.rs +++ b/crates/weaveffi-cli/tests/cli_package.rs @@ -21,7 +21,7 @@ fn write_prebuilt(root: &Path, lib_base: &str) { fn package_bundles_native_libraries_per_ecosystem() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let input = repo_root.join("samples/contacts/contacts.yml"); + let input = repo_root.join("samples/contacts/src/lib.rs"); let bins = tempfile::tempdir().expect("temp bins dir"); write_prebuilt(bins.path(), "contacts"); @@ -85,7 +85,7 @@ fn package_bundles_native_libraries_per_ecosystem() { fn package_skips_unsupported_targets() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let input = repo_root.join("samples/contacts/contacts.yml"); + let input = repo_root.join("samples/contacts/src/lib.rs"); let bins = tempfile::tempdir().expect("temp bins dir"); write_prebuilt(bins.path(), "contacts"); @@ -114,7 +114,7 @@ fn package_skips_unsupported_targets() { fn package_requires_a_binary_source() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let input = repo_root.join("samples/contacts/contacts.yml"); + let input = repo_root.join("samples/contacts/src/lib.rs"); let out_dir = tempfile::tempdir().expect("temp out dir"); // Neither --binaries nor --build: nothing to bundle. diff --git a/crates/weaveffi-cli/tests/cli_project_config.rs b/crates/weaveffi-cli/tests/cli_project_config.rs new file mode 100644 index 00000000..60441cd9 --- /dev/null +++ b/crates/weaveffi-cli/tests/cli_project_config.rs @@ -0,0 +1,294 @@ +//! `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 `[global] c_prefix` reaching all eleven targets. + +use std::fs; +use std::path::{Path, PathBuf}; + +fn repo_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .parent() + .unwrap() + .to_path_buf() +} + +/// Recursively read every file under `dir` and concatenate the text contents. +fn read_tree(dir: &Path) -> String { + let mut out = String::new(); + let Ok(entries) = fs::read_dir(dir) else { + return out; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + out.push_str(&read_tree(&path)); + } else if let Ok(contents) = fs::read_to_string(&path) { + out.push_str(&contents); + out.push('\n'); + } + } + out +} + +fn read(path: &Path) -> String { + fs::read_to_string(path).unwrap_or_else(|e| panic!("missing {}: {e}", path.display())) +} + +fn generate(input: &Path, out: &Path, extra: &[&str]) { + assert_cmd::Command::cargo_bin("weaveffi") + .expect("binary not found") + .args([ + "generate", + input.to_str().unwrap(), + "-o", + out.to_str().unwrap(), + ]) + .args(extra) + .assert() + .success(); +} + +const ALL_TARGETS: [&str; 11] = [ + "c", "cpp", "swift", "android", "node", "wasm", "python", "dotnet", "dart", "go", "ruby", +]; + +/// The kvstore sample's `weaveffi.toml` sits beside its `src/lib.rs`; running +/// on the Rust source must pick it up without `--config` and honor every +/// per-target table plus the `[package]` identity. +#[test] +fn discovers_weaveffi_toml_above_the_input() { + let input = repo_root().join("samples/kvstore/src/lib.rs"); + let dir = tempfile::tempdir().unwrap(); + let out = dir.path(); + generate(&input, out, &[]); + + for target in ALL_TARGETS { + assert!( + out.join(target).is_dir(), + "missing target directory: {target}" + ); + } + + assert!(read(&out.join("cpp/weaveffi.hpp")).contains("namespace kvstore")); + assert!(read(&out.join("dotnet/Kvstore.cs")).contains("namespace Kvstore")); + assert!(out.join("swift/Sources/Kvstore/Kvstore.swift").is_file()); + assert!(read(&out.join("dart/pubspec.yaml")).contains("name: kvstore")); + assert!(read(&out.join("go/go.mod")).contains("github.com/example/kvstore")); + assert!(read(&out.join("ruby/lib/kvstore.rb")).contains("module Kvstore")); + + // `[package]` identity flows into the ecosystem manifests. + let package_json = read(&out.join("node/package.json")); + assert!( + package_json.contains("\"name\": \"kvstore\""), + "{package_json}" + ); + assert!( + package_json.contains("\"version\": \"1.0.0\""), + "{package_json}" + ); + assert!( + package_json.contains("\"license\": \"MIT\""), + "{package_json}" + ); + let pyproject = read(&out.join("python/pyproject.toml")); + assert!(pyproject.contains("name = \"kvstore\""), "{pyproject}"); + assert!(pyproject.contains("version = \"1.0.0\""), "{pyproject}"); +} + +/// `--config` names the file explicitly and wins over discovery. +#[test] +fn explicit_config_overrides_discovery() { + let input = repo_root().join("samples/kvstore/src/lib.rs"); + let dir = tempfile::tempdir().unwrap(); + let cfg = dir.path().join("other.toml"); + fs::write( + &cfg, + concat!( + "[package]\n", + "name = \"renamed\"\n", + "version = \"9.9.9\"\n", + "[generators.cpp]\n", + "namespace = \"elsewhere\"\n", + ), + ) + .unwrap(); + let out = dir.path().join("out"); + generate( + &input, + &out, + &["--target", "cpp,node", "--config", cfg.to_str().unwrap()], + ); + + assert!(read(&out.join("cpp/weaveffi.hpp")).contains("namespace elsewhere")); + let package_json = read(&out.join("node/package.json")); + assert!( + package_json.contains("\"name\": \"renamed\""), + "{package_json}" + ); + assert!( + package_json.contains("\"version\": \"9.9.9\""), + "{package_json}" + ); +} + +/// Without any config, a `lib.rs` input names its package after the crate +/// directory (Cargo's convention), not after the file stem. +#[test] +fn lib_rs_without_config_is_named_after_its_crate_directory() { + let dir = tempfile::tempdir().unwrap(); + let crate_dir = dir.path().join("mycrate").join("src"); + fs::create_dir_all(&crate_dir).unwrap(); + let input = crate_dir.join("lib.rs"); + fs::copy(repo_root().join("samples/calculator/src/lib.rs"), &input).unwrap(); + let out = dir.path().join("out"); + generate(&input, &out, &["--target", "node"]); + let package_json = read(&out.join("node/package.json")); + assert!( + package_json.contains("\"name\": \"mycrate\""), + "{package_json}" + ); +} + +/// A stale `package:` or `generators:` block in an IDL document is rejected +/// with a message that names the offending key, rather than being silently +/// ignored. +#[test] +fn inline_package_block_in_idl_is_an_error() { + let dir = tempfile::tempdir().unwrap(); + let idl = dir.path().join("api.yml"); + fs::write( + &idl, + concat!( + "version: \"0.8.0\"\n", + "package:\n", + " name: legacy\n", + "modules:\n", + " - name: m\n", + " functions: []\n", + ), + ) + .unwrap(); + let output = assert_cmd::Command::cargo_bin("weaveffi") + .expect("binary not found") + .args(["validate", idl.to_str().unwrap()]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("package"), "{stderr}"); +} + +/// Unknown top-level tables in `weaveffi.toml` (such as the pre-0.8 `[swift]` +/// spelling) fail loudly so a misplaced option cannot be silently dropped. +#[test] +fn unknown_config_table_is_an_error() { + let input = repo_root().join("samples/calculator/src/lib.rs"); + 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") + .args([ + "generate", + input.to_str().unwrap(), + "-o", + dir.path().join("out").to_str().unwrap(), + "--config", + cfg.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("swift"), "{stderr}"); + assert!(stderr.contains("failed to parse config file"), "{stderr}"); +} + +/// An unknown `--target` fails instead of silently generating nothing. +#[test] +fn unknown_target_is_an_error() { + let input = repo_root().join("samples/calculator/src/lib.rs"); + let dir = tempfile::tempdir().unwrap(); + let output = assert_cmd::Command::cargo_bin("weaveffi") + .expect("binary not found") + .args([ + "generate", + input.to_str().unwrap(), + "-o", + dir.path().join("out").to_str().unwrap(), + "--target", + "c,rustlang", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("rustlang"), "{stderr}"); +} + +/// The C ABI prefix must reach **every** language backend, not just C/C++: +/// a custom prefix otherwise produces consumer code that links against +/// symbols the (re-prefixed) producer never exported. Generates all targets +/// with `[global] c_prefix = "myffi"` and asserts each one emits the prefixed +/// user symbol and never the default-prefixed one. +#[test] +fn global_c_prefix_propagates_to_all_targets() { + let input = repo_root().join("samples/calculator/src/lib.rs"); + let dir = tempfile::tempdir().unwrap(); + let out = dir.path().join("out"); + let cfg = dir.path().join("cfg.toml"); + fs::write(&cfg, "[global]\nc_prefix = \"myffi\"\n").unwrap(); + generate(&input, &out, &["--config", cfg.to_str().unwrap()]); + + for target in ALL_TARGETS { + let tree = read_tree(&out.join(target)); + assert!( + tree.contains("myffi_calculator_add"), + "target `{target}` did not honor the custom c_prefix" + ); + assert!( + !tree.contains("weaveffi_calculator_add"), + "target `{target}` leaked the default-prefixed user symbol" + ); + } +} + +/// `[generators.c] prefix` alone also fans out (to C++ at least), and the C +/// header aliases the runtime symbols to the custom prefix. +#[test] +fn c_prefix_from_the_c_table_reaches_cpp() { + let input = repo_root().join("samples/calculator/src/lib.rs"); + let dir = tempfile::tempdir().unwrap(); + let out = dir.path().join("out"); + let cfg = dir.path().join("cfg.toml"); + fs::write(&cfg, "[generators.c]\nprefix = \"myffi\"\n").unwrap(); + generate( + &input, + &out, + &["--target", "c,cpp", "--config", cfg.to_str().unwrap()], + ); + + let header = read(&out.join("c/myffi.h")); + for alias in [ + "#define myffi_error weaveffi_error", + "#define myffi_handle_t weaveffi_handle_t", + "#define myffi_free_string weaveffi_free_string", + "#define myffi_cancel_token_create weaveffi_cancel_token_create", + ] { + assert!( + header.contains(alias), + "header missing `{alias}`:\n{header}" + ); + } + assert!(header.contains("myffi_calculator_add")); + assert!(!header.contains("weaveffi_calculator_add")); + + let cpp = read(&out.join("cpp/weaveffi.hpp")); + assert!(cpp.contains("extern \"C\" {")); + assert!(cpp.contains("myffi_calculator_add")); + assert!(!cpp.contains("weaveffi_calculator_add")); + assert!(cpp.contains("#define myffi_error weaveffi_error")); +} diff --git a/crates/weaveffi-cli/tests/cli_python.rs b/crates/weaveffi-cli/tests/cli_python.rs deleted file mode 100644 index 6104a93f..00000000 --- a/crates/weaveffi-cli/tests/cli_python.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::path::Path; - -#[test] -fn generate_python_contacts() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let input = repo_root.join("samples/contacts/contacts.yml"); - - 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") - .args([ - "generate", - input.to_str().unwrap(), - "-o", - out_path.to_str().unwrap(), - "--target", - "python", - ]) - .assert() - .success(); - - let weaveffi_py = std::fs::read_to_string(out_path.join("python/contacts/weaveffi.py")) - .expect("missing python/contacts/weaveffi.py"); - assert!( - weaveffi_py.contains("class ContactBook:"), - "weaveffi.py should contain the ContactBook interface class" - ); - assert!( - weaveffi_py.contains("def __init__(self) -> None:"), - "weaveffi.py should map the `new` constructor to __init__" - ); - assert!( - weaveffi_py.contains( - "def add(self, first_name: str, last_name: str, email: Optional[str], \ - contact_type: \"ContactType\") -> \"Contact\":" - ), - "weaveffi.py should contain the snake_case add method" - ); - assert!( - weaveffi_py.contains("class ContactsError(WeaveFFIError):"), - "weaveffi.py should contain the ContactsError domain class" - ); - assert!( - weaveffi_py.contains("class NotFound(ContactsError):"), - "weaveffi.py should contain the per-code NotFound exception" - ); - - let weaveffi_pyi = std::fs::read_to_string(out_path.join("python/contacts/weaveffi.pyi")) - .expect("missing python/contacts/weaveffi.pyi"); - assert!( - weaveffi_pyi.contains("class ContactBook:"), - "weaveffi.pyi should contain the ContactBook stub" - ); - assert!( - weaveffi_pyi.contains("def get(self, id: int) -> \"Contact\": ..."), - "weaveffi.pyi should contain typed method stubs" - ); - assert!( - weaveffi_pyi.contains("class NotFound(ContactsError):"), - "weaveffi.pyi should contain the typed-error stubs" - ); - - assert!( - out_path.join("python/pyproject.toml").exists(), - "missing python/pyproject.toml" - ); -} diff --git a/crates/weaveffi-cli/tests/cli_ruby.rs b/crates/weaveffi-cli/tests/cli_ruby.rs deleted file mode 100644 index c0ef25e9..00000000 --- a/crates/weaveffi-cli/tests/cli_ruby.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::path::Path; - -#[test] -fn generate_ruby_contacts() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let repo_root = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); - let input = repo_root.join("samples/contacts/contacts.yml"); - - 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") - .args([ - "generate", - input.to_str().unwrap(), - "-o", - out_path.to_str().unwrap(), - "--target", - "ruby", - ]) - .assert() - .success(); - - let rb = std::fs::read_to_string(out_path.join("ruby/lib/contacts.rb")) - .expect("missing ruby/lib/contacts.rb"); - assert!( - rb.contains("module WeaveFFI"), - "contacts.rb should contain module WeaveFFI" - ); - assert!( - rb.contains("module ContactType"), - "contacts.rb should contain ContactType enum" - ); - assert!( - rb.contains("class Contact"), - "contacts.rb should contain Contact class" - ); - assert!( - rb.contains("class ContactBook"), - "contacts.rb should contain ContactBook interface class" - ); - assert!( - rb.contains("class ContactBookPtr < FFI::AutoPointer"), - "contacts.rb should release ContactBook through FFI::AutoPointer" - ); - assert!( - rb.contains("weaveffi_contacts_ContactBook_add"), - "contacts.rb should contain the ContactBook add C symbol" - ); - assert!( - rb.contains("class ContactsError < Error"), - "contacts.rb should contain the typed ContactsError domain" - ); - assert!( - rb.contains("class NotFound < ContactsError"), - "contacts.rb should contain the NotFound code subclass" - ); - assert!( - rb.contains("def self.check_contacts_error!(err)"), - "contacts.rb should contain the typed error checker" - ); - - assert!( - out_path.join("ruby/contacts.gemspec").exists(), - "missing ruby/contacts.gemspec" - ); - assert!( - out_path.join("ruby/README.md").exists(), - "missing ruby/README.md" - ); -} - -#[test] -fn generate_ruby_inventory() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let input = Path::new(manifest_dir).join("tests/fixtures/03_inventory.yml"); - - 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") - .args([ - "generate", - input.to_str().unwrap(), - "-o", - out_path.to_str().unwrap(), - "--target", - "ruby", - ]) - .assert() - .success(); - - let rb = std::fs::read_to_string(out_path.join("ruby/lib/03_inventory.rb")) - .expect("missing ruby/lib/03_inventory.rb"); - assert!( - rb.contains("class Catalog"), - "inventory bindings should contain Catalog interface class" - ); - assert!( - rb.contains("class CatalogPtr < FFI::AutoPointer"), - "inventory bindings should release Catalog through FFI::AutoPointer" - ); - assert!( - rb.contains("class ProductsError < Error"), - "inventory bindings should contain the ProductsError domain" - ); - assert!( - rb.contains("class OrdersError < Error"), - "inventory bindings should contain the OrdersError domain" - ); - // Throwing members route through their module's typed checker; the - // non-throwing `remove` stays on the generic one. - assert!( - rb.contains("WeaveFFI.check_products_error!(err)"), - "Catalog methods should raise typed ProductsError" - ); - assert!( - rb.contains("check_orders_error!(err)"), - "orders functions should raise typed OrdersError" - ); -} diff --git a/crates/weaveffi-cli/tests/cli_schema.rs b/crates/weaveffi-cli/tests/cli_schema.rs index 51eea0e3..02ca481d 100644 --- a/crates/weaveffi-cli/tests/cli_schema.rs +++ b/crates/weaveffi-cli/tests/cli_schema.rs @@ -30,7 +30,6 @@ fn schema_emits_valid_json_schema() { .and_then(|v| v.as_object()) .expect("schema output should include 'definitions'"); for ty in [ - "Package", "Module", "Function", "Param", diff --git a/crates/weaveffi-cli/tests/cli_schema_version.rs b/crates/weaveffi-cli/tests/cli_schema_version.rs index 4852926d..3fac11cb 100644 --- a/crates/weaveffi-cli/tests/cli_schema_version.rs +++ b/crates/weaveffi-cli/tests/cli_schema_version.rs @@ -11,5 +11,5 @@ fn schema_version_command_prints_0_4_0() { String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); - assert_eq!(stdout.trim(), "0.7.0"); + assert_eq!(stdout.trim(), "0.8.0"); } diff --git a/crates/weaveffi-cli/tests/determinism.rs b/crates/weaveffi-cli/tests/determinism.rs index 397f31df..850f2af6 100644 --- a/crates/weaveffi-cli/tests/determinism.rs +++ b/crates/weaveffi-cli/tests/determinism.rs @@ -9,7 +9,7 @@ use std::fs; use std::path::{Path, PathBuf}; use camino::Utf8Path; -use weaveffi_core::codegen::{ConfiguredGenerator, DynGenerator}; +use weaveffi_core::codegen::{ConfiguredBackend, Target}; use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::validate::validate_api; use weaveffi_gen_android::{AndroidConfig, AndroidGenerator}; @@ -25,7 +25,7 @@ use weaveffi_gen_swift::{SwiftConfig, SwiftGenerator}; use weaveffi_gen_wasm::{WasmConfig, WasmGenerator}; use weaveffi_ir::parse::parse_api_str; -const KITCHEN_SINK: &str = "06_kitchen_sink.yml"; +const KITCHEN_SINK: &str = "kitchen_sink.yml"; fn load_kitchen_sink() -> ResolvedApi { let path = Path::new(env!("CARGO_MANIFEST_DIR")) @@ -60,7 +60,7 @@ fn walk(dir: &Path, out: &mut Vec) { } } -fn run_into_tempdir(gen: &dyn DynGenerator, api: &ResolvedApi) -> (tempfile::TempDir, PathBuf) { +fn run_into_tempdir(gen: &dyn Target, api: &ResolvedApi) -> (tempfile::TempDir, PathBuf) { let tmp = tempfile::tempdir().expect("create tempdir"); let out_dir = Utf8Path::from_path(tmp.path()).expect("utf8 tempdir"); gen.generate(api, out_dir).expect("generator failed"); @@ -68,7 +68,7 @@ fn run_into_tempdir(gen: &dyn DynGenerator, api: &ResolvedApi) -> (tempfile::Tem (tmp, gen_root) } -fn assert_byte_identical(gen: &dyn DynGenerator) { +fn assert_byte_identical(gen: &dyn Target) { let api = load_kitchen_sink(); let (_tmp_a, root_a) = run_into_tempdir(gen, &api); @@ -114,42 +114,30 @@ fn assert_byte_identical(gen: &dyn DynGenerator) { #[test] fn generator_output_is_byte_identical_across_runs() { - let generators: Vec> = vec![ - Box::new(ConfiguredGenerator::new(CGenerator, CConfig::default())), - Box::new(ConfiguredGenerator::new(CppGenerator, CppConfig::default())), - Box::new(ConfiguredGenerator::new( + let generators: Vec> = vec![ + Box::new(ConfiguredBackend::new(CGenerator, CConfig::default())), + Box::new(ConfiguredBackend::new(CppGenerator, CppConfig::default())), + Box::new(ConfiguredBackend::new( SwiftGenerator, SwiftConfig::default(), )), - Box::new(ConfiguredGenerator::new( + Box::new(ConfiguredBackend::new( AndroidGenerator, AndroidConfig::default(), )), - Box::new(ConfiguredGenerator::new( - NodeGenerator, - NodeConfig::default(), - )), - Box::new(ConfiguredGenerator::new( - WasmGenerator, - WasmConfig::default(), - )), - Box::new(ConfiguredGenerator::new( + Box::new(ConfiguredBackend::new(NodeGenerator, NodeConfig::default())), + Box::new(ConfiguredBackend::new(WasmGenerator, WasmConfig::default())), + Box::new(ConfiguredBackend::new( PythonGenerator, PythonConfig::default(), )), - Box::new(ConfiguredGenerator::new( + Box::new(ConfiguredBackend::new( DotnetGenerator, DotnetConfig::default(), )), - Box::new(ConfiguredGenerator::new( - DartGenerator, - DartConfig::default(), - )), - Box::new(ConfiguredGenerator::new(GoGenerator, GoConfig::default())), - Box::new(ConfiguredGenerator::new( - RubyGenerator, - RubyConfig::default(), - )), + Box::new(ConfiguredBackend::new(DartGenerator, DartConfig::default())), + Box::new(ConfiguredBackend::new(GoGenerator, GoConfig::default())), + Box::new(ConfiguredBackend::new(RubyGenerator, RubyConfig::default())), ]; for gen in &generators { assert_byte_identical(gen.as_ref()); diff --git a/crates/weaveffi-cli/tests/docs_examples.rs b/crates/weaveffi-cli/tests/docs_examples.rs index 92037dab..27dd5683 100644 --- a/crates/weaveffi-cli/tests/docs_examples.rs +++ b/crates/weaveffi-cli/tests/docs_examples.rs @@ -1,5 +1,5 @@ const GENERATOR_DOCS_YAML: &str = r#" -version: "0.7.0" +version: "0.8.0" modules: - name: contacts enums: @@ -327,7 +327,7 @@ fn summary_md_all_links_resolve() { /// The README quickstart IDL, kept in sync with the `kvstore.yml` snippet in /// `README.md` step 2 (trimmed `Store` interface plus `KvError` domain). const README_QUICKSTART_YAML: &str = r#" -version: "0.7.0" +version: "0.8.0" modules: - name: kv errors: @@ -436,7 +436,7 @@ fn readme_quickstart_generates_c_header() { } const GETTING_STARTED_YAML: &str = r#" -version: "0.7.0" +version: "0.8.0" modules: - name: math structs: @@ -483,7 +483,6 @@ fn getting_started_yaml_generates_all_targets() { yml_path.to_str().unwrap(), "-o", out_path.join("generated").to_str().unwrap(), - "--scaffold", ]) .assert() .success(); @@ -498,7 +497,6 @@ fn getting_started_yaml_generates_all_targets() { gen.join("node/types.d.ts").exists(), "missing node/types.d.ts" ); - assert!(gen.join("scaffold.rs").exists(), "missing scaffold.rs"); let header = std::fs::read_to_string(gen.join("c/weaveffi.h")).unwrap(); assert!( @@ -520,16 +518,6 @@ fn getting_started_yaml_generates_all_targets() { dts.contains("function add(a: number, b: number): number"), "types.d.ts should contain the prefix-stripped add function" ); - - let scaffold = std::fs::read_to_string(gen.join("scaffold.rs")).unwrap(); - assert!( - scaffold.contains("weaveffi_math_add"), - "scaffold should contain weaveffi_math_add" - ); - assert!( - !scaffold.contains("weaveffi_math_Point"), - "scaffold should not emit record stubs" - ); } #[test] @@ -582,13 +570,17 @@ fn readme_states_value_proposition() { #[test] fn readme_uses_only_current_schema_version() { let readme = read_workspace_file("README.md"); + // Only the IDL `version:` key is a schema version; the `weaveffi.toml` + // example's `version = "0.1.0"` is the package version and is fine. assert!( - readme.contains("\"0.7.0\""), - "README should reference schema version 0.7.0" + readme.contains("version: \"0.8.0\""), + "README should reference schema version 0.8.0" ); - for old in ["\"0.1.0\"", "\"0.2.0\"", "\"0.3.0\"", "\"0.4.0\""] { + for old in [ + "0.1.0", "0.2.0", "0.3.0", "0.4.0", "0.5.0", "0.6.0", "0.7.0", + ] { assert!( - !readme.contains(old), + !readme.contains(&format!("version: \"{old}\"")), "README should not reference older schema version {old}" ); } @@ -621,18 +613,13 @@ fn readme_lists_all_eleven_targets_in_table() { fn readme_cli_reference_lists_every_subcommand() { let readme = read_workspace_file("README.md"); for cmd in [ - "weaveffi new", "weaveffi generate", "weaveffi package", "weaveffi validate", - "weaveffi lint", "weaveffi diff", "weaveffi extract", - "weaveffi format", - "weaveffi watch", "weaveffi schema ", "weaveffi schema-version", - "weaveffi doctor", "weaveffi completions", ] { assert!( @@ -640,6 +627,31 @@ fn readme_cli_reference_lists_every_subcommand() { "README CLI reference table missing `{cmd}`" ); } + for removed in [ + "weaveffi new", + "weaveffi lint", + "weaveffi format", + "weaveffi watch", + "weaveffi doctor", + "weaveffi man", + "--scaffold", + ] { + assert!( + !readme.contains(removed), + "README still documents the removed `{removed}`" + ); + } +} + +#[test] +fn readme_documents_project_config_file() { + let readme = read_workspace_file("README.md"); + for needle in ["weaveffi.toml", "[package]", "[global]", "[generators."] { + assert!( + readme.contains(needle), + "README project-config section missing `{needle}`" + ); + } } #[test] diff --git a/crates/weaveffi-cli/tests/extract_roundtrip.rs b/crates/weaveffi-cli/tests/extract_roundtrip.rs index 50d43389..b153a479 100644 --- a/crates/weaveffi-cli/tests/extract_roundtrip.rs +++ b/crates/weaveffi-cli/tests/extract_roundtrip.rs @@ -3,7 +3,7 @@ //! `roundtrip_kitchen_sink` proves the extractor recovers the same shape //! as the original kitchen-sink IDL when run on the hand-annotated Rust //! file at `crates/weaveffi-cli/tests/fixtures/kitchen_sink_annotated.rs`, -//! including the 0.7.0 surface: the `Gadget` interface, the `KitchenErrors` +//! including the full surface: the `Gadget` interface, the `KitchenErrors` //! domain, and per-function `throws`. Lossy fields (struct field defaults, //! iterator returns, standalone `since` without `#[deprecated]`, callback //! param docs, error-code `doc:` separate from `message:`) are documented @@ -15,23 +15,15 @@ use std::collections::BTreeMap; use weaveffi_ir::ir::{Api, Function, InterfaceDef, Module, StructDef, TypeRef}; use weaveffi_ir::parse::parse_api_str; -/// The validator that runs inside `weaveffi extract` rewrites cross-module -/// struct refs (e.g. `Token` → `shared.Token`) and resolves each bare name -/// to its kind (`Record`, `RichEnum`, `Enum`, `Interface`). Neither rewrite -/// survives the YAML round-trip because a string like `"Priority"` always -/// re-parses as `Named("Priority")` regardless of which kind it was. Compare -/// types modulo those two transforms so a fresh parse matches a validated -/// one. +/// Compare user-type references by their final path segment so a +/// module-qualified spelling (`shared.Token`) matches the bare one the +/// extractor emits for an in-scope Rust type. fn normalize(ty: &TypeRef) -> TypeRef { fn last_segment(name: &str) -> String { name.rsplit('.').next().unwrap_or(name).to_string() } match ty { - TypeRef::Named(name) - | TypeRef::Record(name) - | TypeRef::RichEnum(name) - | TypeRef::Enum(name) - | TypeRef::Interface(name) => TypeRef::Named(last_segment(name)), + TypeRef::Named(name) => TypeRef::Named(last_segment(name)), TypeRef::TypedHandle(name) => TypeRef::TypedHandle(last_segment(name)), TypeRef::Optional(inner) => TypeRef::Optional(Box::new(normalize(inner))), TypeRef::List(inner) => TypeRef::List(Box::new(normalize(inner))), @@ -110,7 +102,7 @@ fn assert_functions_equivalent(extracted: &Function, orig: &Function, ctx: &str) #[test] fn roundtrip_kitchen_sink() { let manifest = env!("CARGO_MANIFEST_DIR"); - let idl_path = format!("{manifest}/tests/fixtures/06_kitchen_sink.yml"); + let idl_path = format!("{manifest}/tests/fixtures/kitchen_sink.yml"); let annotated_path = format!("{manifest}/tests/fixtures/kitchen_sink_annotated.rs"); let original_src = std::fs::read_to_string(&idl_path).expect("read kitchen-sink IDL"); diff --git a/crates/weaveffi-cli/tests/fixtures/01_calculator.yml b/crates/weaveffi-cli/tests/fixtures/01_calculator.yml deleted file mode 100644 index f2240bf0..00000000 --- a/crates/weaveffi-cli/tests/fixtures/01_calculator.yml +++ /dev/null @@ -1,32 +0,0 @@ -version: "0.7.0" -modules: - - name: calculator - errors: - name: CalcError - codes: - - { name: DivisionByZero, code: 1, message: "division by zero" } - functions: - - name: add - doc: Add two integers - params: - - { name: a, type: i32 } - - { name: b, type: i32 } - return: i32 - - name: mul - doc: Multiply two integers - params: - - { name: a, type: i32 } - - { name: b, type: i32 } - return: i32 - - name: div - doc: Divide two integers, failing on a zero divisor - params: - - { name: a, type: i32 } - - { name: b, type: i32 } - return: i32 - throws: true - - name: echo - doc: Echo a string back - params: - - { name: s, type: string } - return: string diff --git a/crates/weaveffi-cli/tests/fixtures/02_contacts.yml b/crates/weaveffi-cli/tests/fixtures/02_contacts.yml deleted file mode 100644 index 70952201..00000000 --- a/crates/weaveffi-cli/tests/fixtures/02_contacts.yml +++ /dev/null @@ -1,66 +0,0 @@ -version: "0.7.0" -modules: - - name: contacts - enums: - - name: ContactType - variants: - - name: Personal - value: 0 - - name: Work - value: 1 - - name: Other - value: 2 - structs: - - name: Contact - fields: - - name: id - type: i64 - - name: first_name - type: string - - name: last_name - type: string - - name: email - type: "string?" - - name: contact_type - type: ContactType - errors: - name: ContactsError - codes: - - { name: InvalidName, code: 1, message: "name must not be empty" } - - { name: NotFound, code: 2, message: "contact not found" } - interfaces: - - name: ContactBook - doc: An in-memory address book owning its contacts - constructors: - - name: new - params: [] - methods: - - name: add - params: - - name: first_name - type: string - - name: last_name - type: string - - name: email - type: "string?" - - name: contact_type - type: ContactType - return: Contact - throws: true - - name: get - params: - - name: id - type: i64 - return: Contact - throws: true - - name: list - params: [] - return: "[Contact]" - - name: remove - params: - - name: id - type: i64 - return: bool - - name: count - params: [] - return: i32 diff --git a/crates/weaveffi-cli/tests/fixtures/03_inventory.yml b/crates/weaveffi-cli/tests/fixtures/03_inventory.yml deleted file mode 100644 index 67fe726d..00000000 --- a/crates/weaveffi-cli/tests/fixtures/03_inventory.yml +++ /dev/null @@ -1,125 +0,0 @@ -version: "0.7.0" -modules: - - name: products - enums: - - name: Category - variants: - - name: Electronics - value: 0 - - name: Clothing - value: 1 - - name: Food - value: 2 - - name: Books - value: 3 - structs: - - name: Product - fields: - - name: id - type: i64 - - name: name - type: string - - name: description - type: "string?" - - name: price - type: f64 - - name: category - type: Category - - name: tags - type: "[string]" - errors: - name: ProductsError - codes: - - { name: InvalidPrice, code: 1, message: "price must be positive" } - - { name: ProductNotFound, code: 2, message: "product not found" } - interfaces: - - name: Catalog - doc: A product catalog owning its products - constructors: - - name: new - params: [] - methods: - - name: add_product - params: - - name: name - type: string - - name: price - type: f64 - - name: category - type: Category - return: Product - throws: true - - name: get_product - params: - - name: id - type: i64 - return: Product - throws: true - - name: search - params: - - name: category - type: Category - return: "[Product]" - - name: update_price - params: - - name: id - type: i64 - - name: price - type: f64 - return: bool - throws: true - - name: remove - params: - - name: id - type: i64 - return: bool - - name: orders - structs: - - name: OrderItem - fields: - - name: product_id - type: i64 - - name: quantity - type: i32 - - name: unit_price - type: f64 - - name: Order - fields: - - name: id - type: i64 - - name: items - type: "[OrderItem]" - - name: total - type: f64 - - name: status - type: string - errors: - name: OrdersError - codes: - - { name: OrderNotFound, code: 1, message: "order not found" } - - { name: EmptyOrder, code: 2, message: "order must contain at least one item" } - functions: - - name: create_order - params: - - name: items - type: "[OrderItem]" - return: i64 - throws: true - - name: get_order - params: - - name: id - type: i64 - return: Order - throws: true - - name: cancel_order - params: - - name: id - type: i64 - return: bool - - name: add_product_to_order - params: - - name: order_id - type: i64 - - name: product - type: Product - return: bool diff --git a/crates/weaveffi-cli/tests/fixtures/04_async_demo.yml b/crates/weaveffi-cli/tests/fixtures/04_async_demo.yml deleted file mode 100644 index 35f5e0b6..00000000 --- a/crates/weaveffi-cli/tests/fixtures/04_async_demo.yml +++ /dev/null @@ -1,37 +0,0 @@ -version: "0.7.0" -modules: - - name: tasks - structs: - - name: TaskResult - fields: - - { name: id, type: i64 } - - { name: value, type: string } - - { name: success, type: bool } - errors: - name: TaskError - codes: - - { name: InvalidName, code: 1, message: "task name must not be empty" } - functions: - - name: run_task - params: - - { name: name, type: string } - return: TaskResult - async: true - throws: true - - name: run_batch - params: - - { name: names, type: "[string]" } - return: "[TaskResult]" - async: true - - name: cancel_task - params: - - { name: id, type: i64 } - return: bool - - name: run_n_tasks - params: - - { name: n, type: i32 } - return: i32 - async: true - - name: active_callbacks - params: [] - return: i64 diff --git a/crates/weaveffi-cli/tests/fixtures/05_events.yml b/crates/weaveffi-cli/tests/fixtures/05_events.yml deleted file mode 100644 index 772a2bb8..00000000 --- a/crates/weaveffi-cli/tests/fixtures/05_events.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: "0.7.0" -modules: - - name: events - callbacks: - - name: OnMessage - params: - - { name: message, type: string } - listeners: - - name: message_listener - event_callback: OnMessage - functions: - - name: send_message - doc: Send a message, triggering the OnMessage callback - params: - - { name: text, type: string } - - name: get_messages - doc: Return an iterator over all sent messages - params: [] - return: "iter" diff --git a/crates/weaveffi-cli/tests/fixtures/08_kvstore.yml b/crates/weaveffi-cli/tests/fixtures/08_kvstore.yml deleted file mode 100644 index 8cc09c5f..00000000 --- a/crates/weaveffi-cli/tests/fixtures/08_kvstore.yml +++ /dev/null @@ -1,117 +0,0 @@ -version: "0.7.0" -modules: - - name: kv - structs: - - name: Entry - doc: A single key-value entry persisted in the store - fields: - - { name: id, type: i64, doc: Stable monotonic identifier assigned on insert } - - { name: key, type: string, doc: UTF-8 lookup key } - - { name: value, type: bytes, doc: Opaque binary payload } - - { name: created_at, type: i64, doc: Unix-timestamp seconds when the entry was created } - - { name: expires_at, type: "i64?", doc: Optional unix-timestamp seconds at which the entry expires } - - { name: tags, type: "[string]", doc: Free-form labels attached to the entry } - - { name: metadata, type: "{string:string}", doc: Arbitrary string-valued metadata pairs } - enums: - - name: EntryKind - doc: Persistence semantics applied to a stored entry - variants: - - { name: Volatile, value: 0, doc: In-memory only; lost on close } - - { name: Persistent, value: 1, doc: Flushed to durable storage } - - { name: Encrypted, value: 2, doc: Persistent and encrypted at rest } - callbacks: - - name: OnEvict - doc: Fires when an entry is evicted from the store - params: - - { name: key, type: string } - listeners: - - name: eviction_listener - event_callback: OnEvict - doc: Subscribe to per-key eviction notifications - errors: - name: KvError - codes: - - { name: KeyNotFound, code: 1001, message: "key not found", doc: No entry exists for the requested key } - - { name: Expired, code: 1002, message: "entry expired", doc: The entry exists but its TTL has elapsed } - - { name: StoreFull, code: 1003, message: "store has reached capacity", doc: The store has reached its capacity limit } - - { name: IoError, code: 1004, message: "I/O failure", doc: Underlying storage reported an I/O failure } - interfaces: - - name: Store - doc: An in-memory key-value store owning its entries; destroying the object releases all of its state - constructors: - - name: open - doc: Open (or create) a store backed by the given filesystem path - params: - - { name: path, type: string } - throws: true - methods: - - name: put - doc: Insert or replace a value, returning true on success - params: - - { name: key, type: string } - - { name: value, type: bytes } - - { name: kind, type: EntryKind } - - { name: ttl_seconds, type: "i64?" } - return: bool - throws: true - - name: get - doc: Look up an entry by key; returns null if missing or expired - params: - - { name: key, type: string } - return: "Entry?" - throws: true - - name: delete - doc: Remove the entry for the given key, returning true if it existed - params: - - { name: key, type: string } - return: bool - throws: true - - name: list_keys - doc: Stream every key, optionally filtered by a prefix - params: - - { name: prefix, type: "string?" } - return: "iter" - throws: true - - name: count - doc: Return the number of live entries in the store - params: [] - return: i64 - - name: clear - doc: Drop every entry from the store - params: [] - - name: compact - doc: Reclaim space asynchronously; returns the number of bytes reclaimed - async: true - cancellable: true - params: [] - return: i64 - throws: true - - name: legacy_put - doc: Legacy single-shot put kept for compatibility - deprecated: "use put() with explicit kind" - params: - - { name: key, type: string } - - { name: value, type: bytes } - return: bool - throws: true - statics: - - name: default_capacity - doc: The largest number of live entries one store will hold - params: [] - return: i64 - modules: - - name: stats - structs: - - name: Stats - doc: Aggregate store statistics - fields: - - { name: total_entries, type: i64, doc: Number of live entries } - - { name: total_bytes, type: i64, doc: Sum of all value byte lengths } - - { name: expired_entries, type: i64, doc: Number of entries past their TTL but not yet evicted } - functions: - - name: get_stats - doc: Snapshot the current store statistics - params: - - { name: store, type: Store } - return: Stats - throws: true diff --git a/crates/weaveffi-cli/tests/fixtures/07_docs_everywhere.yml b/crates/weaveffi-cli/tests/fixtures/docs_everywhere.yml similarity index 99% rename from crates/weaveffi-cli/tests/fixtures/07_docs_everywhere.yml rename to crates/weaveffi-cli/tests/fixtures/docs_everywhere.yml index 297184b9..9781741a 100644 --- a/crates/weaveffi-cli/tests/fixtures/07_docs_everywhere.yml +++ b/crates/weaveffi-cli/tests/fixtures/docs_everywhere.yml @@ -1,4 +1,4 @@ -version: "0.7.0" +version: "0.8.0" modules: - name: docs enums: diff --git a/crates/weaveffi-cli/tests/fixtures/edge_cases.yml b/crates/weaveffi-cli/tests/fixtures/edge_cases.yml new file mode 100644 index 00000000..50557517 --- /dev/null +++ b/crates/weaveffi-cli/tests/fixtures/edge_cases.yml @@ -0,0 +1,282 @@ +version: "0.8.0" +# Shapes the mainstream fixtures do not reach: identifiers that are reserved +# words in one or more target languages, deeply nested composites, optional and +# mutable parameters, interfaces in every legal position, async functions with +# non-string results, type-level deprecation, scalar and string iterators, and +# callbacks carrying composite payloads. Snapshots pin how every generator +# renders each of these, so a regression in keyword escaping or a codec edge +# shows up as a diff instead of as a compile error in a downstream project. +modules: + - name: edge_cases + enums: + - name: Legacy + doc: Deprecated enum with non-contiguous values and keyword-like variants + deprecated: "Use Mode instead" + variants: + - { name: None, value: 0 } + - { name: Default, value: 5 } + - { name: Class, value: 10 } + - name: Mode + variants: + - { name: Fast, value: 1 } + - { name: Safe, value: 2 } + structs: + - name: Leaf + fields: + - { name: weight, type: f32 } + - name: Inner + doc: Middle layer of a three-deep struct nesting + fields: + - { name: leaf, type: Leaf } + - { name: leaves, type: "[Leaf]" } + - name: Outer + fields: + - { name: inner, type: Inner } + - { name: maybe_inner, type: "Inner?" } + - { name: by_name, type: "{string:Inner}" } + - { name: modes, type: "[Mode]" } + - { name: matrix, type: "[[i32]]" } + - { name: sparse, type: "[i64?]" } + - { name: blob, type: "bytes?" } + - { name: note, type: "string?" } + - { name: by_id, type: "{i64:string}" } + - { name: _hidden, type: bool } + - { name: v2, type: u8 } + - name: Keywords + doc: Every field is a reserved word somewhere + fields: + - { name: class, type: i32 } + - { name: def, type: i32 } + - { name: import, type: i32 } + - { name: from, type: i32 } + - { name: func, type: i32 } + - { name: range, type: i32 } + - { name: register, type: i32 } + - { name: volatile, type: i32 } + - { name: new, type: i32 } + - { name: default, type: i32 } + - { name: end, type: i32 } + - { name: in, type: i32 } + - { name: is, type: i32 } + - { name: int, type: i32 } + - { name: var, type: i32 } + - { name: val, type: i32 } + - { name: self, type: i32 } + - { name: this, type: i32 } + - { name: object, type: i32 } + - { name: string, type: i32 } + - { name: case, type: i32 } + - { name: select, type: i32 } + - { name: switch, type: i32 } + - { name: void, type: i32 } + - { name: union, type: i32 } + - { name: namespace, type: i32 } + - { name: operator, type: i32 } + - { name: template, type: i32 } + - { name: package, type: i32 } + - { name: protocol, type: i32 } + - { name: guard, type: i32 } + - { name: defer, type: i32 } + - { name: inout, type: i32 } + - { name: internal, type: i32 } + - { name: when, type: i32 } + - { name: fun, type: i32 } + - { name: data, type: i32 } + - { name: late, type: i32 } + - { name: part, type: i32 } + - { name: out, type: i32 } + - { name: ref, type: i32 } + - { name: event, type: i32 } + - { name: base, type: i32 } + - { name: lambda, type: i32 } + - { name: yield, type: i32 } + - { name: nil, type: i32 } + - { name: null, type: i32 } + - { name: begin, type: i32 } + - { name: rescue, type: i32 } + - { name: module, type: i32 } + - { name: chan, type: i32 } + - { name: go, type: i32 } + - { name: let, type: i32 } + - { name: export, type: i32 } + - { name: delete, type: i32 } + - { name: typedef, type: i32 } + - { name: sizeof, type: i32 } + - { name: static, type: i32 } + - { name: super, type: i32 } + - name: Retired + doc: Deprecated struct + deprecated: "Use Outer instead" + fields: + - { name: id, type: i64 } + callbacks: + - name: OnBatch + doc: Callback carrying composite payloads + params: + - { name: items, type: "[Outer]" } + - { name: raw, type: bytes } + - { name: first, type: "Outer?" } + - { name: mode, type: Mode } + - name: OnTick + params: [] + listeners: + - name: batch_listener + event_callback: OnBatch + - name: tick_listener + event_callback: OnTick + errors: + name: EdgeErrors + codes: + - { name: Busy, code: 7, message: "Resource is busy" } + interfaces: + - name: Widget + doc: Interface with no constructor; instances come from make_widget + methods: + - name: refresh + doc: Void method that can fail + params: [] + throws: true + - name: rename + params: + - { name: class, type: string } + - { name: import, type: "string?" } + return: string + - name: fetch + doc: Async method with a struct result + params: + - { name: id, type: i64 } + return: Outer + async: true + throws: true + - name: old_name + deprecated: "Use rename instead" + params: [] + return: string + - name: Vault + doc: Deprecated interface with a throwing constructor + deprecated: "Use Widget instead" + constructors: + - name: open + doc: Fails when the vault is busy + params: + - { name: path, type: string } + throws: true + methods: + - name: peek + params: [] + return: "Widget?" + statics: + - name: probe + doc: Throwing static + params: [] + return: bool + throws: true + functions: + - name: noop + doc: No parameters and no result + params: [] + - name: select + doc: Function named after a keyword whose parameters are all keywords + params: + - { name: class, type: i32 } + - { name: from, type: string } + - { name: func, type: "[string]" } + - { name: range, type: "i32?" } + - { name: type_, type: bool } + return: i32 + - name: nested_optional + doc: Optional list of optional structs, the deepest legal nesting + params: + - { name: items, type: "[Outer?]?" } + return: "[Outer?]?" + - name: optional_params + params: + - { name: text, type: "string?" } + - { name: count, type: "i32?" } + - { name: outer, type: "Outer?" } + - { name: tags, type: "[string]?" } + - { name: blob, type: "bytes?" } + - { name: mode, type: "Mode?" } + return: "Mode?" + - name: mutable_params + doc: Write-back parameters + params: + - { name: text, type: string, mutable: true } + - { name: blob, type: bytes, mutable: true } + - name: make_widget + doc: Factory for an interface with no constructor + params: + - { name: label, type: string } + return: Widget + - name: find_widget + doc: Optional interface return + params: + - { name: id, type: i64 } + return: "Widget?" + throws: true + - name: inspect_widget + doc: Interface parameter, including an optional one + params: + - { name: widget, type: Widget } + - { name: fallback, type: "Widget?" } + return: string + - name: typed_handle_param + params: + - { name: token, type: "handle" } + - { name: raw, type: handle } + return: "handle" + - name: string_stream + doc: Iterator of strings + params: [] + return: "iter" + - name: scalar_stream + doc: Iterator of scalars + params: [] + return: "iter" + - name: mode_stream + doc: Iterator of enums + params: [] + return: "iter" + - name: async_void + doc: Async with no result + params: [] + async: true + - name: async_bytes + params: + - { name: size, type: u32 } + return: bytes + async: true + - name: async_struct + params: [] + return: Outer + async: true + throws: true + - name: async_list + params: [] + return: "[Outer]" + async: true + cancellable: true + throws: true + - name: async_optional + params: [] + return: "i64?" + async: true + - name: async_enum + params: [] + return: Mode + async: true + - name: async_keywords + params: + - { name: class, type: string } + - { name: import, type: i32 } + return: string + async: true + - name: keywords_roundtrip + params: + - { name: value, type: Keywords } + return: Keywords + - name: legacy_value + deprecated: "Use Mode-based APIs" + params: + - { name: value, type: Legacy } + return: Retired diff --git a/crates/weaveffi-cli/tests/fixtures/06_kitchen_sink.yml b/crates/weaveffi-cli/tests/fixtures/kitchen_sink.yml similarity index 99% rename from crates/weaveffi-cli/tests/fixtures/06_kitchen_sink.yml rename to crates/weaveffi-cli/tests/fixtures/kitchen_sink.yml index 1804ff6b..da9bdb61 100644 --- a/crates/weaveffi-cli/tests/fixtures/06_kitchen_sink.yml +++ b/crates/weaveffi-cli/tests/fixtures/kitchen_sink.yml @@ -1,4 +1,4 @@ -version: "0.7.0" +version: "0.8.0" modules: - name: shared structs: diff --git a/crates/weaveffi-cli/tests/fixtures/09_nested_modules.yml b/crates/weaveffi-cli/tests/fixtures/nested_modules.yml similarity index 99% rename from crates/weaveffi-cli/tests/fixtures/09_nested_modules.yml rename to crates/weaveffi-cli/tests/fixtures/nested_modules.yml index 5122288a..6dd87059 100644 --- a/crates/weaveffi-cli/tests/fixtures/09_nested_modules.yml +++ b/crates/weaveffi-cli/tests/fixtures/nested_modules.yml @@ -1,4 +1,4 @@ -version: "0.7.0" +version: "0.8.0" # Exercises multi-level nested modules and type references that cross nesting # boundaries in every direction: # * parent -> nested child (graphics.Canvas.root : shapes.Shape) diff --git a/crates/weaveffi-cli/tests/fixtures/10_shapes.yml b/crates/weaveffi-cli/tests/fixtures/shapes.yml similarity index 99% rename from crates/weaveffi-cli/tests/fixtures/10_shapes.yml rename to crates/weaveffi-cli/tests/fixtures/shapes.yml index 8fd89244..028e252b 100644 --- a/crates/weaveffi-cli/tests/fixtures/10_shapes.yml +++ b/crates/weaveffi-cli/tests/fixtures/shapes.yml @@ -1,4 +1,4 @@ -version: "0.7.0" +version: "0.8.0" modules: - name: shapes doc: Rich (algebraic) enums and the expanded numeric primitives. diff --git a/crates/weaveffi-cli/tests/no_silent_stubs.rs b/crates/weaveffi-cli/tests/no_silent_stubs.rs index eded9b70..f58fdfe9 100644 --- a/crates/weaveffi-cli/tests/no_silent_stubs.rs +++ b/crates/weaveffi-cli/tests/no_silent_stubs.rs @@ -43,9 +43,9 @@ fn generator_sources_ban_unimplemented_and_todo() { for entry in fs::read_dir(&crates_dir).expect("read crates/") { let path = entry.expect("dir entry").path(); let name = path.file_name().unwrap().to_string_lossy().to_string(); - // The CLI's `scaffold` output intentionally emits `todo!()` for the - // *user's* Rust producer skeleton, so only generator + core crates - // (the code that produces consumer bindings) are in scope. + // Only generator + core crates (the code that produces consumer + // bindings) are in scope; test helpers and the CLI are free to use + // placeholder macros. if !(name.starts_with("weaveffi-gen-") || name == "weaveffi-core") { continue; } @@ -86,9 +86,9 @@ fn generator_sources_ban_unimplemented_and_todo() { fn generated_output_has_no_stub_markers() { let root = workspace_root(); let samples = [ - root.join("samples/contacts/contacts.yml"), - root.join("samples/events/events.yml"), - root.join("samples/kvstore/kvstore.yml"), + root.join("samples/contacts/src/lib.rs"), + root.join("samples/events/src/lib.rs"), + root.join("samples/kvstore/src/lib.rs"), ]; // Case-insensitive marker list. Bare "not supported" is deliberately diff --git a/crates/weaveffi-cli/tests/safety_audit.rs b/crates/weaveffi-cli/tests/safety_audit.rs index 7eac37a1..598d0dd6 100644 --- a/crates/weaveffi-cli/tests/safety_audit.rs +++ b/crates/weaveffi-cli/tests/safety_audit.rs @@ -1,5 +1,5 @@ fn audit_api_yaml() -> &'static str { - r#"version: "0.7.0" + r#"version: "0.8.0" modules: - name: inventory structs: diff --git a/crates/weaveffi-cli/tests/sample_drift.rs b/crates/weaveffi-cli/tests/sample_drift.rs deleted file mode 100644 index 164db6d3..00000000 --- a/crates/weaveffi-cli/tests/sample_drift.rs +++ /dev/null @@ -1,201 +0,0 @@ -//! The sample drift gate: every macro-annotated sample crate must extract to -//! the same API shape its hand-written YAML IDL declares. -//! -//! The samples ship both authoring paths (a `#[weaveffi::module]` `lib.rs` -//! and a YAML IDL), and the conformance harness builds producers from the -//! macro path while generating consumers from the YAML path. Nothing but -//! this test forces the two views to agree, and a silent mismatch means the -//! consumers call symbols the producer never exported (or marshal a shape it -//! doesn't produce). -//! -//! The comparison is structural: module tree, function signatures and flags, -//! record fields, enum variants and values, callbacks, listeners, interfaces, -//! and error domains (names, codes, and messages, which cross the ABI). -//! Prose doc comments are deliberately not gated; they're cosmetic, differ in -//! phrasing between the two sources, and can't break a consumer. Struct -//! field defaults and standalone `since:` values are documented extract -//! gaps (see `docs/src/guides/extract.md`) and are skipped too. - -use weaveffi_ir::ir::{Api, Function, Module, StructField, TypeRef}; -use weaveffi_ir::parse::parse_api_str; - -/// Fold a type to the shape that survives an extract-then-reparse cycle: -/// resolved kinds (`Record`, `Enum`, `Interface`, ...) re-parse as `Named`, -/// and validator-qualified cross-module names (`shared.Token`) reduce to -/// their final segment. -fn normalize_type(ty: &TypeRef) -> TypeRef { - fn last_segment(name: &str) -> String { - name.rsplit('.').next().unwrap_or(name).to_string() - } - match ty { - TypeRef::Named(name) - | TypeRef::Record(name) - | TypeRef::RichEnum(name) - | TypeRef::Enum(name) - | TypeRef::Interface(name) => TypeRef::Named(last_segment(name)), - TypeRef::TypedHandle(name) => TypeRef::TypedHandle(last_segment(name)), - TypeRef::Optional(inner) => TypeRef::Optional(Box::new(normalize_type(inner))), - TypeRef::List(inner) => TypeRef::List(Box::new(normalize_type(inner))), - TypeRef::Iterator(inner) => TypeRef::Iterator(Box::new(normalize_type(inner))), - TypeRef::Map(k, v) => { - TypeRef::Map(Box::new(normalize_type(k)), Box::new(normalize_type(v))) - } - other => other.clone(), - } -} - -/// Strip everything the gate doesn't compare from one function. -fn canon_function(f: &mut Function) { - f.doc = None; - // A standalone `since:` (without `#[deprecated]`) has no Rust spelling. - if f.deprecated.is_none() { - f.since = None; - } - for p in &mut f.params { - p.doc = None; - p.ty = normalize_type(&p.ty); - } - if let Some(ret) = &mut f.returns { - *ret = normalize_type(ret); - } -} - -/// Strip everything the gate doesn't compare from one field. -fn canon_field(field: &mut StructField) { - field.doc = None; - field.ty = normalize_type(&field.ty); -} - -/// Strip everything the gate doesn't compare from one module, recursively. -fn canon_module(m: &mut Module) { - for f in &mut m.functions { - canon_function(f); - } - for i in &mut m.interfaces { - i.doc = None; - for f in i - .constructors - .iter_mut() - .chain(i.methods.iter_mut()) - .chain(i.statics.iter_mut()) - { - canon_function(f); - } - } - for s in &mut m.structs { - s.doc = None; - for field in &mut s.fields { - canon_field(field); - } - } - for e in &mut m.enums { - e.doc = None; - for v in &mut e.variants { - v.doc = None; - for field in &mut v.fields { - canon_field(field); - } - } - } - for c in &mut m.callbacks { - c.doc = None; - for p in &mut c.params { - p.doc = None; - p.ty = normalize_type(&p.ty); - } - } - for l in &mut m.listeners { - l.doc = None; - } - if let Some(errors) = &mut m.errors { - for code in &mut errors.codes { - // Codes, names, and messages cross the ABI and are compared; a - // separate `doc:` next to `message:` has no Rust spelling. - code.doc = None; - for field in &mut code.fields { - canon_field(field); - } - } - } - for child in &mut m.modules { - canon_module(child); - } -} - -/// The canonical, comparable form of an API: modules only, docs and lossy -/// fields stripped, types normalized. -fn canon_api(api: &Api) -> Vec { - let mut modules = api.modules.clone(); - for m in &mut modules { - canon_module(m); - } - modules -} - -#[test] -fn samples_never_drift_from_their_idl() { - let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let samples_dir = manifest - .parent() - .and_then(|p| p.parent()) - .expect("workspace root") - .join("samples"); - - let mut gated = 0usize; - for entry in std::fs::read_dir(&samples_dir).expect("read samples dir") { - let dir = entry.expect("sample dir entry").path(); - if !dir.is_dir() { - continue; - } - let lib_rs = dir.join("src/lib.rs"); - let Ok(src) = std::fs::read_to_string(&lib_rs) else { - continue; - }; - if !src.contains("weaveffi::module") { - continue; - } - let Some(yml) = std::fs::read_dir(&dir) - .expect("read sample dir") - .filter_map(|e| e.ok().map(|e| e.path())) - .find(|p| { - p.extension() - .is_some_and(|ext| ext == "yml" || ext == "yaml") - }) - else { - continue; - }; - - let sample = dir.file_name().unwrap().to_string_lossy().to_string(); - let idl_src = std::fs::read_to_string(&yml).expect("read sample IDL"); - let original = parse_api_str(&idl_src, "yaml").expect("parse sample IDL"); - - let output = assert_cmd::Command::cargo_bin("weaveffi") - .expect("weaveffi binary") - .args(["extract", lib_rs.to_str().unwrap(), "-f", "yaml"]) - .output() - .expect("run weaveffi extract"); - assert!( - output.status.success(), - "[{sample}] extract failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - let extracted_yaml = String::from_utf8(output.stdout).expect("extract stdout utf-8"); - let extracted = parse_api_str(&extracted_yaml, "yaml").expect("parse extracted YAML"); - - assert_eq!( - canon_api(&extracted), - canon_api(&original), - "[{sample}] the macro-annotated lib.rs and the YAML IDL have \ - drifted; update whichever side is stale so both declare the \ - same API shape" - ); - gated += 1; - } - - // If this trips, the discovery walk broke (or samples moved); the gate - // must never silently pass by gating nothing. - assert!( - gated >= 7, - "expected at least 7 gated samples, found {gated}" - ); -} diff --git a/crates/weaveffi-cli/tests/snapshots.rs b/crates/weaveffi-cli/tests/snapshots.rs index f6bd9c43..0d96d8d3 100644 --- a/crates/weaveffi-cli/tests/snapshots.rs +++ b/crates/weaveffi-cli/tests/snapshots.rs @@ -1,15 +1,28 @@ -//! Snapshot tests covering every generator against a representative IDL corpus. +//! Snapshot tests covering every generator against a small, feature-complete +//! IDL corpus. //! -//! Each test parses a fixture, runs a single generator into a fresh tempdir, -//! walks the resulting files in sorted order, and snapshots their contents -//! one-file-per-snapshot under `tests/snapshots/`. Regressions in any -//! generator's output cause the affected `cargo insta test` job to fail. +//! Five fixtures cover the whole IDL surface between them: `kitchen_sink` +//! (every scalar and composite type, borrowed params, handles, an interface, +//! callbacks and a listener, 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 and mutable parameters, interfaces in every legal position, +//! async functions with non-string results, type-level deprecation, and +//! scalar and string iterators). One test per generator runs it over all +//! five fixtures into a fresh tempdir, walks the +//! resulting files in sorted order, and snapshots each file under +//! `tests/snapshots/`. Regressions in any generator's output fail the +//! affected `cargo insta test` job; behavioral regressions are the +//! conformance harness's job. use std::fs; use std::path::{Path, PathBuf}; use camino::Utf8Path; -use weaveffi_core::codegen::{ConfiguredGenerator, DynGenerator, Generator}; +use weaveffi_core::codegen::{ConfiguredBackend, Target}; use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::validate::validate_api; use weaveffi_gen_android::{AndroidConfig, AndroidGenerator}; @@ -25,14 +38,18 @@ use weaveffi_gen_swift::{SwiftConfig, SwiftGenerator}; use weaveffi_gen_wasm::{WasmConfig, WasmGenerator}; use weaveffi_ir::parse::parse_api_str; -fn fixture_path(file: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("tests/fixtures") - .join(file) -} +const FIXTURES: [&str; 5] = [ + "kitchen_sink", + "shapes", + "nested_modules", + "docs_everywhere", + "edge_cases", +]; -fn load_api(file: &str) -> ResolvedApi { - let path = fixture_path(file); +fn load_api(stem: &str) -> ResolvedApi { + let path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures") + .join(format!("{stem}.yml")); let contents = fs::read_to_string(&path) .unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display())); let api = parse_api_str(&contents, "yaml") @@ -41,35 +58,32 @@ fn load_api(file: &str) -> ResolvedApi { } fn collect_files_sorted(root: &Path) -> Vec { + fn walk(dir: &Path, out: &mut Vec) { + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + walk(&path, out); + } else if path.is_file() { + out.push(path); + } + } + } let mut out = Vec::new(); walk(root, &mut out); out.sort(); out } -fn walk(dir: &Path, out: &mut Vec) { - let entries = match fs::read_dir(dir) { - Ok(e) => e, - Err(_) => return, - }; - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - walk(&path, out); - } else if path.is_file() { - out.push(path); - } - } -} - fn sanitize(rel: &Path) -> String { rel.to_string_lossy().replace(['/', '\\', '.', '-'], "_") } /// Replace the concrete crate version in every WeaveFFI prelude -/// (`Generated by WeaveFFI 0.7.0 from ...`) with a stable `[VERSION]` -/// placeholder. Without this, bumping the crate version invalidates all -/// 300+ generator snapshots even though no generated logic changed. +/// (`Generated by WeaveFFI 0.21.0 from ...`) with a stable `[VERSION]` +/// placeholder so a release bump does not invalidate every snapshot. fn redact_version(s: &str) -> String { const MARKER: &str = "Generated by WeaveFFI "; let mut out = String::with_capacity(s.len()); @@ -85,786 +99,75 @@ fn redact_version(s: &str) -> String { out } -fn run_snapshot(gen: &G, fixture_stem: &str, fixture_file: &str) -where - G::Config: Default, -{ - let api = load_api(fixture_file); - let tmp = tempfile::tempdir().expect("create tempdir"); - let out_dir = Utf8Path::from_path(tmp.path()).expect("utf8 tempdir"); - let config = G::Config::default(); - gen.generate(&api, out_dir, &config) - .expect("generator failed"); - - let gen_root = out_dir.join(gen.name()); - let files = collect_files_sorted(gen_root.as_std_path()); - assert!( - !files.is_empty(), - "generator {} produced no files for fixture {}", - gen.name(), - fixture_file, - ); - - insta::with_settings!({ - snapshot_path => "snapshots", - prepend_module_to_snapshot => false, - omit_expression => true, - }, { - for file in files { - let rel = file - .strip_prefix(gen_root.as_std_path()) - .expect("file under gen root"); - let name = format!("{}_{}__{}", gen.name(), fixture_stem, sanitize(rel)); - let contents = fs::read_to_string(&file) - .unwrap_or_else(|e| panic!("read {}: {e}", file.display())); - // Snapshot the version-redacted form so a release bump (e.g. - // 0.7.0 → 0.7.0) does not invalidate every snapshot; assert the - // real prelude (with the concrete version) against the original. - insta::assert_snapshot!(name, redact_version(&contents)); - assert_prelude_present(&contents, &file); - } - }); -} - -/// Asserts every generated file embeds the standard WeaveFFI prelude markers -/// in its first five lines. JSON files (which cannot carry comments) embed -/// these markers via the `"//"` key convention. +/// Every generated file embeds the standard WeaveFFI prelude markers in its +/// first five lines. JSON files (which cannot carry comments) embed these +/// markers via the `"//"` key convention. fn assert_prelude_present(contents: &str, file: &Path) { let head: String = contents.lines().take(5).collect::>().join("\n"); - assert!( - head.contains("Generated by WeaveFFI"), - "{} is missing 'Generated by WeaveFFI' in the first 5 lines:\n{head}", - file.display(), - ); - assert!( - head.contains("DO NOT EDIT"), - "{} is missing 'DO NOT EDIT' in the first 5 lines:\n{head}", - file.display(), - ); -} - -macro_rules! snapshot_test { - ($fn_name:ident, $gen_expr:expr, $fixture_stem:literal, $fixture_file:literal) => { - #[test] - fn $fn_name() { - run_snapshot(&$gen_expr, $fixture_stem, $fixture_file); - } - }; + for marker in ["Generated by WeaveFFI", "DO NOT EDIT"] { + assert!( + head.contains(marker), + "{} is missing '{marker}' in the first 5 lines:\n{head}", + file.display(), + ); + } } -snapshot_test!( - snapshot_c_calculator, - CGenerator, - "calculator", - "01_calculator.yml" -); -snapshot_test!( - snapshot_c_contacts, - CGenerator, - "contacts", - "02_contacts.yml" -); -snapshot_test!( - snapshot_c_inventory, - CGenerator, - "inventory", - "03_inventory.yml" -); -snapshot_test!( - snapshot_c_async_demo, - CGenerator, - "async_demo", - "04_async_demo.yml" -); -snapshot_test!(snapshot_c_events, CGenerator, "events", "05_events.yml"); -snapshot_test!( - snapshot_c_kitchen_sink, - CGenerator, - "kitchen_sink", - "06_kitchen_sink.yml" -); -snapshot_test!( - snapshot_c_docs_everywhere, - CGenerator, - "docs_everywhere", - "07_docs_everywhere.yml" -); -snapshot_test!(snapshot_c_kvstore, CGenerator, "kvstore", "08_kvstore.yml"); -snapshot_test!( - snapshot_c_nested_modules, - CGenerator, - "nested_modules", - "09_nested_modules.yml" -); - -snapshot_test!( - snapshot_cpp_calculator, - CppGenerator, - "calculator", - "01_calculator.yml" -); -snapshot_test!( - snapshot_cpp_contacts, - CppGenerator, - "contacts", - "02_contacts.yml" -); -snapshot_test!( - snapshot_cpp_inventory, - CppGenerator, - "inventory", - "03_inventory.yml" -); -snapshot_test!( - snapshot_cpp_async_demo, - CppGenerator, - "async_demo", - "04_async_demo.yml" -); -snapshot_test!(snapshot_cpp_events, CppGenerator, "events", "05_events.yml"); -snapshot_test!( - snapshot_cpp_kitchen_sink, - CppGenerator, - "kitchen_sink", - "06_kitchen_sink.yml" -); -snapshot_test!( - snapshot_cpp_docs_everywhere, - CppGenerator, - "docs_everywhere", - "07_docs_everywhere.yml" -); -snapshot_test!( - snapshot_cpp_kvstore, - CppGenerator, - "kvstore", - "08_kvstore.yml" -); -snapshot_test!( - snapshot_cpp_nested_modules, - CppGenerator, - "nested_modules", - "09_nested_modules.yml" -); - -snapshot_test!( - snapshot_swift_calculator, - SwiftGenerator, - "calculator", - "01_calculator.yml" -); -snapshot_test!( - snapshot_swift_contacts, - SwiftGenerator, - "contacts", - "02_contacts.yml" -); -snapshot_test!( - snapshot_swift_inventory, - SwiftGenerator, - "inventory", - "03_inventory.yml" -); -snapshot_test!( - snapshot_swift_async_demo, - SwiftGenerator, - "async_demo", - "04_async_demo.yml" -); -snapshot_test!( - snapshot_swift_events, - SwiftGenerator, - "events", - "05_events.yml" -); -snapshot_test!( - snapshot_swift_kitchen_sink, - SwiftGenerator, - "kitchen_sink", - "06_kitchen_sink.yml" -); -snapshot_test!( - snapshot_swift_docs_everywhere, - SwiftGenerator, - "docs_everywhere", - "07_docs_everywhere.yml" -); -snapshot_test!( - snapshot_swift_kvstore, - SwiftGenerator, - "kvstore", - "08_kvstore.yml" -); -snapshot_test!( - snapshot_swift_nested_modules, - SwiftGenerator, - "nested_modules", - "09_nested_modules.yml" -); - -snapshot_test!( - snapshot_android_calculator, - AndroidGenerator, - "calculator", - "01_calculator.yml" -); -snapshot_test!( - snapshot_android_contacts, - AndroidGenerator, - "contacts", - "02_contacts.yml" -); -snapshot_test!( - snapshot_android_inventory, - AndroidGenerator, - "inventory", - "03_inventory.yml" -); -snapshot_test!( - snapshot_android_async_demo, - AndroidGenerator, - "async_demo", - "04_async_demo.yml" -); -snapshot_test!( - snapshot_android_events, - AndroidGenerator, - "events", - "05_events.yml" -); -snapshot_test!( - snapshot_android_kitchen_sink, - AndroidGenerator, - "kitchen_sink", - "06_kitchen_sink.yml" -); -snapshot_test!( - snapshot_android_docs_everywhere, - AndroidGenerator, - "docs_everywhere", - "07_docs_everywhere.yml" -); -snapshot_test!( - snapshot_android_kvstore, - AndroidGenerator, - "kvstore", - "08_kvstore.yml" -); -snapshot_test!( - snapshot_android_nested_modules, - AndroidGenerator, - "nested_modules", - "09_nested_modules.yml" -); - -snapshot_test!( - snapshot_node_calculator, - NodeGenerator, - "calculator", - "01_calculator.yml" -); -snapshot_test!( - snapshot_node_contacts, - NodeGenerator, - "contacts", - "02_contacts.yml" -); -snapshot_test!( - snapshot_node_inventory, - NodeGenerator, - "inventory", - "03_inventory.yml" -); -snapshot_test!( - snapshot_node_async_demo, - NodeGenerator, - "async_demo", - "04_async_demo.yml" -); -snapshot_test!( - snapshot_node_events, - NodeGenerator, - "events", - "05_events.yml" -); -snapshot_test!( - snapshot_node_kitchen_sink, - NodeGenerator, - "kitchen_sink", - "06_kitchen_sink.yml" -); -snapshot_test!( - snapshot_node_docs_everywhere, - NodeGenerator, - "docs_everywhere", - "07_docs_everywhere.yml" -); -snapshot_test!( - snapshot_node_kvstore, - NodeGenerator, - "kvstore", - "08_kvstore.yml" -); -snapshot_test!( - snapshot_node_nested_modules, - NodeGenerator, - "nested_modules", - "09_nested_modules.yml" -); - -snapshot_test!( - snapshot_wasm_calculator, - WasmGenerator, - "calculator", - "01_calculator.yml" -); -snapshot_test!( - snapshot_wasm_contacts, - WasmGenerator, - "contacts", - "02_contacts.yml" -); -snapshot_test!( - snapshot_wasm_inventory, - WasmGenerator, - "inventory", - "03_inventory.yml" -); -snapshot_test!( - snapshot_wasm_async_demo, - WasmGenerator, - "async_demo", - "04_async_demo.yml" -); -snapshot_test!( - snapshot_wasm_events, - WasmGenerator, - "events", - "05_events.yml" -); -snapshot_test!( - snapshot_wasm_kitchen_sink, - WasmGenerator, - "kitchen_sink", - "06_kitchen_sink.yml" -); -snapshot_test!( - snapshot_wasm_docs_everywhere, - WasmGenerator, - "docs_everywhere", - "07_docs_everywhere.yml" -); -snapshot_test!( - snapshot_wasm_kvstore, - WasmGenerator, - "kvstore", - "08_kvstore.yml" -); -snapshot_test!( - snapshot_wasm_nested_modules, - WasmGenerator, - "nested_modules", - "09_nested_modules.yml" -); - -snapshot_test!( - snapshot_python_calculator, - PythonGenerator, - "calculator", - "01_calculator.yml" -); -snapshot_test!( - snapshot_python_contacts, - PythonGenerator, - "contacts", - "02_contacts.yml" -); -snapshot_test!( - snapshot_python_inventory, - PythonGenerator, - "inventory", - "03_inventory.yml" -); -snapshot_test!( - snapshot_python_async_demo, - PythonGenerator, - "async_demo", - "04_async_demo.yml" -); -snapshot_test!( - snapshot_python_events, - PythonGenerator, - "events", - "05_events.yml" -); -snapshot_test!( - snapshot_python_kitchen_sink, - PythonGenerator, - "kitchen_sink", - "06_kitchen_sink.yml" -); -snapshot_test!( - snapshot_python_docs_everywhere, - PythonGenerator, - "docs_everywhere", - "07_docs_everywhere.yml" -); -snapshot_test!( - snapshot_python_kvstore, - PythonGenerator, - "kvstore", - "08_kvstore.yml" -); -snapshot_test!( - snapshot_python_nested_modules, - PythonGenerator, - "nested_modules", - "09_nested_modules.yml" -); - -snapshot_test!( - snapshot_dotnet_calculator, - DotnetGenerator, - "calculator", - "01_calculator.yml" -); -snapshot_test!( - snapshot_dotnet_contacts, - DotnetGenerator, - "contacts", - "02_contacts.yml" -); -snapshot_test!( - snapshot_dotnet_inventory, - DotnetGenerator, - "inventory", - "03_inventory.yml" -); -snapshot_test!( - snapshot_dotnet_async_demo, - DotnetGenerator, - "async_demo", - "04_async_demo.yml" -); -snapshot_test!( - snapshot_dotnet_events, - DotnetGenerator, - "events", - "05_events.yml" -); -snapshot_test!( - snapshot_dotnet_kitchen_sink, - DotnetGenerator, - "kitchen_sink", - "06_kitchen_sink.yml" -); -snapshot_test!( - snapshot_dotnet_docs_everywhere, - DotnetGenerator, - "docs_everywhere", - "07_docs_everywhere.yml" -); -snapshot_test!( - snapshot_dotnet_kvstore, - DotnetGenerator, - "kvstore", - "08_kvstore.yml" -); -snapshot_test!( - snapshot_dotnet_nested_modules, - DotnetGenerator, - "nested_modules", - "09_nested_modules.yml" -); - -snapshot_test!( - snapshot_dart_calculator, - DartGenerator, - "calculator", - "01_calculator.yml" -); -snapshot_test!( - snapshot_dart_contacts, - DartGenerator, - "contacts", - "02_contacts.yml" -); -snapshot_test!( - snapshot_dart_inventory, - DartGenerator, - "inventory", - "03_inventory.yml" -); -snapshot_test!( - snapshot_dart_async_demo, - DartGenerator, - "async_demo", - "04_async_demo.yml" -); -snapshot_test!( - snapshot_dart_events, - DartGenerator, - "events", - "05_events.yml" -); -snapshot_test!( - snapshot_dart_kitchen_sink, - DartGenerator, - "kitchen_sink", - "06_kitchen_sink.yml" -); -snapshot_test!( - snapshot_dart_docs_everywhere, - DartGenerator, - "docs_everywhere", - "07_docs_everywhere.yml" -); -snapshot_test!( - snapshot_dart_kvstore, - DartGenerator, - "kvstore", - "08_kvstore.yml" -); -snapshot_test!( - snapshot_dart_nested_modules, - DartGenerator, - "nested_modules", - "09_nested_modules.yml" -); - -snapshot_test!( - snapshot_go_calculator, - GoGenerator, - "calculator", - "01_calculator.yml" -); -snapshot_test!( - snapshot_go_contacts, - GoGenerator, - "contacts", - "02_contacts.yml" -); -snapshot_test!( - snapshot_go_inventory, - GoGenerator, - "inventory", - "03_inventory.yml" -); -snapshot_test!( - snapshot_go_async_demo, - GoGenerator, - "async_demo", - "04_async_demo.yml" -); -snapshot_test!(snapshot_go_events, GoGenerator, "events", "05_events.yml"); -snapshot_test!( - snapshot_go_kitchen_sink, - GoGenerator, - "kitchen_sink", - "06_kitchen_sink.yml" -); -snapshot_test!( - snapshot_go_docs_everywhere, - GoGenerator, - "docs_everywhere", - "07_docs_everywhere.yml" -); -snapshot_test!( - snapshot_go_kvstore, - GoGenerator, - "kvstore", - "08_kvstore.yml" -); -snapshot_test!( - snapshot_go_nested_modules, - GoGenerator, - "nested_modules", - "09_nested_modules.yml" -); - -snapshot_test!( - snapshot_ruby_calculator, - RubyGenerator, - "calculator", - "01_calculator.yml" -); -snapshot_test!( - snapshot_ruby_contacts, - RubyGenerator, - "contacts", - "02_contacts.yml" -); -snapshot_test!( - snapshot_ruby_inventory, - RubyGenerator, - "inventory", - "03_inventory.yml" -); -snapshot_test!( - snapshot_ruby_async_demo, - RubyGenerator, - "async_demo", - "04_async_demo.yml" -); -snapshot_test!( - snapshot_ruby_events, - RubyGenerator, - "events", - "05_events.yml" -); -snapshot_test!( - snapshot_ruby_kitchen_sink, - RubyGenerator, - "kitchen_sink", - "06_kitchen_sink.yml" -); -snapshot_test!( - snapshot_ruby_docs_everywhere, - RubyGenerator, - "docs_everywhere", - "07_docs_everywhere.yml" -); -snapshot_test!( - snapshot_ruby_kvstore, - RubyGenerator, - "kvstore", - "08_kvstore.yml" -); -snapshot_test!( - snapshot_ruby_nested_modules, - RubyGenerator, - "nested_modules", - "09_nested_modules.yml" -); - -// Rich (algebraic) enums + the expanded numeric primitives, across every -// backend (fixture 10). Locks the rich-enum wrapper surface (opaque object, -// per-variant constructors and field getters, tag reader, destructor) into the -// snapshot oracle for all 11 generators. -snapshot_test!(snapshot_c_shapes, CGenerator, "shapes", "10_shapes.yml"); -snapshot_test!(snapshot_cpp_shapes, CppGenerator, "shapes", "10_shapes.yml"); -snapshot_test!( - snapshot_swift_shapes, - SwiftGenerator, - "shapes", - "10_shapes.yml" -); -snapshot_test!( - snapshot_android_shapes, - AndroidGenerator, - "shapes", - "10_shapes.yml" -); -snapshot_test!( - snapshot_node_shapes, - NodeGenerator, - "shapes", - "10_shapes.yml" -); -snapshot_test!( - snapshot_wasm_shapes, - WasmGenerator, - "shapes", - "10_shapes.yml" -); -snapshot_test!( - snapshot_python_shapes, - PythonGenerator, - "shapes", - "10_shapes.yml" -); -snapshot_test!( - snapshot_dotnet_shapes, - DotnetGenerator, - "shapes", - "10_shapes.yml" -); -snapshot_test!( - snapshot_dart_shapes, - DartGenerator, - "shapes", - "10_shapes.yml" -); -snapshot_test!(snapshot_go_shapes, GoGenerator, "shapes", "10_shapes.yml"); -snapshot_test!( - snapshot_ruby_shapes, - RubyGenerator, - "shapes", - "10_shapes.yml" -); - -/// Phase 5 prelude coverage: every generator, every fixture, every output file -/// must carry the standard WeaveFFI prelude markers in its first five lines. -#[test] -fn prelude_present_in_every_generated_file() { - let generators: Vec> = vec![ - Box::new(ConfiguredGenerator::new(CGenerator, CConfig::default())), - Box::new(ConfiguredGenerator::new(CppGenerator, CppConfig::default())), - Box::new(ConfiguredGenerator::new( - SwiftGenerator, - SwiftConfig::default(), - )), - Box::new(ConfiguredGenerator::new( - AndroidGenerator, - AndroidConfig::default(), - )), - Box::new(ConfiguredGenerator::new( - NodeGenerator, - NodeConfig::default(), - )), - Box::new(ConfiguredGenerator::new( - WasmGenerator, - WasmConfig::default(), - )), - Box::new(ConfiguredGenerator::new( - PythonGenerator, - PythonConfig::default(), - )), - Box::new(ConfiguredGenerator::new( - DotnetGenerator, - DotnetConfig::default(), - )), - Box::new(ConfiguredGenerator::new( - DartGenerator, - DartConfig::default(), - )), - Box::new(ConfiguredGenerator::new(GoGenerator, GoConfig::default())), - Box::new(ConfiguredGenerator::new( - RubyGenerator, - RubyConfig::default(), - )), - ]; - let fixtures = [ - "01_calculator.yml", - "02_contacts.yml", - "03_inventory.yml", - "04_async_demo.yml", - "05_events.yml", - "06_kitchen_sink.yml", - "07_docs_everywhere.yml", - "08_kvstore.yml", - "09_nested_modules.yml", - "10_shapes.yml", - ]; - - for fixture in fixtures { - let api = load_api(fixture); - for gen in &generators { - let tmp = tempfile::tempdir().expect("create tempdir"); - let out_dir = Utf8Path::from_path(tmp.path()).expect("utf8 tempdir"); - gen.generate(&api, out_dir).expect("generator failed"); - let gen_root = out_dir.join(gen.name()); - let files = collect_files_sorted(gen_root.as_std_path()); - assert!( - !files.is_empty(), - "generator {} produced no files for fixture {fixture}", - gen.name(), - ); +fn run_snapshots(target: &dyn Target) { + for stem in FIXTURES { + let api = load_api(stem); + let tmp = tempfile::tempdir().expect("create tempdir"); + let out_dir = Utf8Path::from_path(tmp.path()).expect("utf8 tempdir"); + target.generate(&api, out_dir).expect("generator failed"); + + let root = out_dir.join(target.name()); + let files = collect_files_sorted(root.as_std_path()); + assert!( + !files.is_empty(), + "generator {} produced no files for fixture {stem}", + target.name(), + ); + + insta::with_settings!({ + snapshot_path => "snapshots", + prepend_module_to_snapshot => false, + omit_expression => true, + }, { for file in files { + let rel = file + .strip_prefix(root.as_std_path()) + .expect("file under generator root"); + let name = format!("{}_{stem}__{}", target.name(), sanitize(rel)); let contents = fs::read_to_string(&file) .unwrap_or_else(|e| panic!("read {}: {e}", file.display())); + insta::assert_snapshot!(name, redact_version(&contents)); assert_prelude_present(&contents, &file); } - } + }); } } + +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_android => AndroidGenerator, AndroidConfig; + 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; +} diff --git a/crates/weaveffi-cli/tests/snapshots/android_async_demo__src_main_cpp_weaveffi_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/android_async_demo__src_main_cpp_weaveffi_jni_c.snap deleted file mode 100644 index 1959b2e9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_async_demo__src_main_cpp_weaveffi_jni_c.snap +++ /dev/null @@ -1,217 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include -#include -#include -#include -#include -#include "weaveffi.h" - -static void throw_weaveffi_error(JNIEnv* env, weaveffi_error* err) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jclass exClass = (*env)->FindClass(env, "com/weaveffi/WeaveFFIException"); - if (exClass != NULL) { - jmethodID ctor = (*env)->GetMethodID(env, exClass, "", "(ILjava/lang/String;)V"); - jstring jmsg = (*env)->NewStringUTF(env, msg); - jthrowable ex = (jthrowable)(*env)->NewObject(env, exClass, ctor, (jint)err->code, jmsg); - if (ex != NULL) { (*env)->Throw(env, ex); } - } - weaveffi_error_clear(err); -} - -typedef struct { - JavaVM* jvm; - jobject callback; -} weaveffi_jni_async_ctx; - -static jclass weaveffi_jni_entry_class = NULL; -static jmethodID weaveffi_jni_dispatch_exc = NULL; - -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { - (void)reserved; - JNIEnv* env = NULL; - if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } - jclass cls = (*env)->FindClass(env, "com/weaveffi/WeaveFFI"); - if (cls == NULL) { (*env)->ExceptionClear(env); return JNI_VERSION_1_6; } - weaveffi_jni_entry_class = (jclass)(*env)->NewGlobalRef(env, cls); - weaveffi_jni_dispatch_exc = (*env)->GetStaticMethodID(env, weaveffi_jni_entry_class, "dispatchCallbackException", "(Ljava/lang/Throwable;)V"); - if (weaveffi_jni_dispatch_exc == NULL) { (*env)->ExceptionClear(env); } - return JNI_VERSION_1_6; -} - -static void weaveffi_jni_handle_uncaught(JNIEnv* env) { - if (!(*env)->ExceptionCheck(env)) { return; } - jthrowable ex = (*env)->ExceptionOccurred(env); - (*env)->ExceptionClear(env); - if (weaveffi_jni_entry_class != NULL && weaveffi_jni_dispatch_exc != NULL) { - (*env)->CallStaticVoidMethod(env, weaveffi_jni_entry_class, weaveffi_jni_dispatch_exc, ex); - if (!(*env)->ExceptionCheck(env)) { (*env)->DeleteLocalRef(env, ex); return; } - (*env)->ExceptionClear(env); - } - (*env)->Throw(env, ex); - (*env)->ExceptionDescribe(env); - (*env)->ExceptionClear(env); - (*env)->DeleteLocalRef(env, ex); -} - -static void weaveffi_tasks_run_task_jni_cb(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { - weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)context; - JNIEnv* env = NULL; - int attached = 0; - if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { - if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) { free(ctx); return; } - attached = 1; - } - if (err != NULL && err->code != 0) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = (*env)->NewStringUTF(env, msg); - jbyteArray jpayload = NULL; - if (err->payload_ptr != NULL) { - jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); - if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } - } - jint jcode = (jint)err->code; - weaveffi_error_free(err); - jclass cls = (*env)->GetObjectClass(env, ctx->callback); - jmethodID mid = (*env)->GetMethodID(env, cls, "onError", "(ILjava/lang/String;[B)V"); - (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); - } else { - jbyteArray boxed = (*env)->NewByteArray(env, (jsize)result_len); - if (boxed && result_ptr) { (*env)->SetByteArrayRegion(env, boxed, 0, (jsize)result_len, (const jbyte*)result_ptr); } - weaveffi_free_bytes((uint8_t*)result_ptr, result_len); - jclass cls = (*env)->GetObjectClass(env, ctx->callback); - jmethodID mid = (*env)->GetMethodID(env, cls, "onSuccess", "(Ljava/lang/Object;)V"); - (*env)->CallVoidMethod(env, ctx->callback, mid, boxed); - } - weaveffi_jni_handle_uncaught(env); - (*env)->DeleteGlobalRef(env, ctx->callback); - JavaVM* jvm = ctx->jvm; - free(ctx); - if (attached) (*jvm)->DetachCurrentThread(jvm); -} - -JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_runTaskAsync(JNIEnv* env, jclass clazz, jstring name, jobject callback) { - weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx)); - (*env)->GetJavaVM(env, &ctx->jvm); - ctx->callback = (*env)->NewGlobalRef(env, callback); - const char* name_chars = (*env)->GetStringUTFChars(env, name, NULL); - weaveffi_tasks_run_task_async(name_chars, weaveffi_tasks_run_task_jni_cb, ctx); - (*env)->ReleaseStringUTFChars(env, name, name_chars); -} - -static void weaveffi_tasks_run_batch_jni_cb(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { - weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)context; - JNIEnv* env = NULL; - int attached = 0; - if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { - if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) { free(ctx); return; } - attached = 1; - } - if (err != NULL && err->code != 0) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = (*env)->NewStringUTF(env, msg); - jbyteArray jpayload = NULL; - if (err->payload_ptr != NULL) { - jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); - if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } - } - jint jcode = (jint)err->code; - weaveffi_error_free(err); - jclass cls = (*env)->GetObjectClass(env, ctx->callback); - jmethodID mid = (*env)->GetMethodID(env, cls, "onError", "(ILjava/lang/String;[B)V"); - (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); - } else { - jbyteArray boxed = (*env)->NewByteArray(env, (jsize)result_len); - if (boxed && result_ptr) { (*env)->SetByteArrayRegion(env, boxed, 0, (jsize)result_len, (const jbyte*)result_ptr); } - weaveffi_free_bytes((uint8_t*)result_ptr, result_len); - jclass cls = (*env)->GetObjectClass(env, ctx->callback); - jmethodID mid = (*env)->GetMethodID(env, cls, "onSuccess", "(Ljava/lang/Object;)V"); - (*env)->CallVoidMethod(env, ctx->callback, mid, boxed); - } - weaveffi_jni_handle_uncaught(env); - (*env)->DeleteGlobalRef(env, ctx->callback); - JavaVM* jvm = ctx->jvm; - free(ctx); - if (attached) (*jvm)->DetachCurrentThread(jvm); -} - -JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_runBatchAsync(JNIEnv* env, jclass clazz, jbyteArray names, jobject callback) { - weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx)); - (*env)->GetJavaVM(env, &ctx->jvm); - ctx->callback = (*env)->NewGlobalRef(env, callback); - jbyte* names_elems = (*env)->GetByteArrayElements(env, names, NULL); - jsize names_len = (*env)->GetArrayLength(env, names); - weaveffi_tasks_run_batch_async((const uint8_t*)names_elems, (size_t)names_len, weaveffi_tasks_run_batch_jni_cb, ctx); - (*env)->ReleaseByteArrayElements(env, names, names_elems, JNI_ABORT); -} - -JNIEXPORT jboolean JNICALL Java_com_weaveffi_WeaveFFI_cancelTask(JNIEnv* env, jclass clazz, jlong id) { - weaveffi_error err = {0, NULL, NULL, 0}; - bool rv = weaveffi_tasks_cancel_task((int64_t)id, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return JNI_FALSE; - } - return rv ? JNI_TRUE : JNI_FALSE; -} - -static void weaveffi_tasks_run_n_tasks_jni_cb(void* context, weaveffi_error* err, int32_t result) { - weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)context; - JNIEnv* env = NULL; - int attached = 0; - if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { - if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) { free(ctx); return; } - attached = 1; - } - if (err != NULL && err->code != 0) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = (*env)->NewStringUTF(env, msg); - jbyteArray jpayload = NULL; - if (err->payload_ptr != NULL) { - jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); - if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } - } - jint jcode = (jint)err->code; - weaveffi_error_free(err); - jclass cls = (*env)->GetObjectClass(env, ctx->callback); - jmethodID mid = (*env)->GetMethodID(env, cls, "onError", "(ILjava/lang/String;[B)V"); - (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); - } else { - jclass boxCls = (*env)->FindClass(env, "java/lang/Integer"); - jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(I)Ljava/lang/Integer;"); - jobject boxed = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, (jint)result); - jclass cls = (*env)->GetObjectClass(env, ctx->callback); - jmethodID mid = (*env)->GetMethodID(env, cls, "onSuccess", "(Ljava/lang/Object;)V"); - (*env)->CallVoidMethod(env, ctx->callback, mid, boxed); - } - weaveffi_jni_handle_uncaught(env); - (*env)->DeleteGlobalRef(env, ctx->callback); - JavaVM* jvm = ctx->jvm; - free(ctx); - if (attached) (*jvm)->DetachCurrentThread(jvm); -} - -JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_runNTasksAsync(JNIEnv* env, jclass clazz, jint n, jobject callback) { - weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx)); - (*env)->GetJavaVM(env, &ctx->jvm); - ctx->callback = (*env)->NewGlobalRef(env, callback); - weaveffi_tasks_run_n_tasks_async((int32_t)n, weaveffi_tasks_run_n_tasks_jni_cb, ctx); -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_activeCallbacks(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - int64_t rv = weaveffi_tasks_active_callbacks(&err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jlong) rv; -} - - -// END weaveffi_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/android_async_demo__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap b/crates/weaveffi-cli/tests/snapshots/android_async_demo__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap deleted file mode 100644 index a7006b29..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_async_demo__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap +++ /dev/null @@ -1,244 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -package com.weaveffi - -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - -class WeaveFFI { - companion object { - init { System.loadLibrary("weaveffi") } - - @Volatile private var callbackExceptionHandler: ((Throwable) -> Unit)? = null - - /** - * Installs a handler for exceptions thrown by listener callbacks and - * async continuations on native producer threads. These exceptions have - * no Kotlin caller to propagate to; when no handler is installed, they - * are logged with their stack trace and dropped. Pass `null` to - * restore the default logging behavior. - */ - @JvmStatic fun setCallbackExceptionHandler(handler: ((Throwable) -> Unit)?) { - callbackExceptionHandler = handler - } - - // Invoked from the JNI glue; rethrowing signals "no handler" so the - // glue falls back to ExceptionDescribe. - @JvmStatic private fun dispatchCallbackException(t: Throwable) { - callbackExceptionHandler?.invoke(t) ?: throw t - } - - @JvmStatic private external fun runTaskAsync(name: String, callback: Any) - @JvmStatic suspend fun runTask(name: String): TaskResult { - val raw: ByteArray = suspendCancellableCoroutine { cont -> - runTaskAsync(name, WeaveContinuation(cont) { code, message, payload -> TaskException.fromCode(code, message, payload) }) - } - return weaveDecode(raw) { r -> unpackTaskResult(r) } - } - @JvmStatic private external fun runBatchAsync(names: ByteArray, callback: Any) - @JvmStatic suspend fun runBatch(names: List): List { - val raw: ByteArray = suspendCancellableCoroutine { cont -> - runBatchAsync(weaveEncode { w -> w.writeList(names) { v0 -> w.writeString(v0) } }, WeaveContinuation(cont) { code, message, _ -> WeaveFFIException(code, message) }) - } - return weaveDecode(raw) { r -> r.readList { unpackTaskResult(r) } } - } - @JvmStatic external fun cancelTask(id: Long): Boolean - @JvmStatic private external fun runNTasksAsync(n: Int, callback: Any) - @JvmStatic suspend fun runNTasks(n: Int): Int = suspendCancellableCoroutine { cont -> - runNTasksAsync(n, WeaveContinuation(cont) { code, message, _ -> WeaveFFIException(code, message) }) - } - @JvmStatic external fun activeCallbacks(): Long - } -} - -data class TaskResult(val id: Long, val value: String, val success: Boolean) - -internal fun packTaskResult(w: WeaveBufferWriter, v: TaskResult) { - w.writeI64(v.id) - w.writeString(v.value) - w.writeBool(v.success) -} - -internal fun unpackTaskResult(r: WeaveBufferReader): TaskResult = TaskResult( - r.readI64(), - r.readString(), - r.readBool(), -) - -/** Generic WeaveFFI failure: panics, marshalling errors, and unknown codes. */ -open class WeaveFFIException(val code: Int, message: String) : Exception(message) - -/** Typed error domain `TaskError` declared by module `tasks`. */ -sealed class TaskException(code: Int, message: String) : WeaveFFIException(code, message) { - class InvalidName(message: String = "task name must not be empty") : TaskException(1, message) - - companion object { - /** Map a raw `TaskError` code and payload to the typed exception; unknown codes yield the generic [WeaveFFIException]. */ - @JvmStatic fun fromCode(code: Int, message: String, payload: ByteArray?): WeaveFFIException = when (code) { - 1 -> InvalidName(message) - else -> WeaveFFIException(code, message) - } - } -} - -/** Growable little-endian writer implementing the WeaveFFI value-buffer wire format. */ -internal class WeaveBufferWriter { - private var buf = ByteArray(32) - private var len = 0 - - 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 writeI16(v: Short) { - reserve(2) - val b = v.toInt() - buf[len++] = (b and 0xFF).toByte() - buf[len++] = ((b shr 8) and 0xFF).toByte() - } - fun writeI32(v: Int) { - reserve(4) - buf[len++] = (v and 0xFF).toByte() - buf[len++] = ((v shr 8) and 0xFF).toByte() - buf[len++] = ((v shr 16) and 0xFF).toByte() - buf[len++] = ((v shr 24) and 0xFF).toByte() - } - fun writeU32(v: Long) = writeI32(v.toInt()) - fun writeI64(v: Long) { - writeI32(v.toInt()) - writeI32((v ushr 32).toInt()) - } - fun writeF32(v: Float) = writeI32(v.toRawBits()) - fun writeF64(v: Double) = writeI64(v.toRawBits()) - fun writeString(v: String) = writeBytes(v.toByteArray(Charsets.UTF_8)) - fun writeBytes(v: ByteArray) { - writeI32(v.size) - reserve(v.size) - v.copyInto(buf, len) - len += v.size - } - fun writeOptional(v: T?, write: (T) -> Unit) { - if (v == null) writeBool(false) else { writeBool(true); write(v) } - } - fun writeList(v: List, write: (T) -> Unit) { - writeI32(v.size) - for (e in v) write(e) - } - 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 WeaveFFI value-buffer wire format. */ -internal class WeaveBufferReader(private val buf: ByteArray) { - private var pos = 0 - - private fun malformed(detail: String): Nothing = - throw WeaveFFIException(-2, "malformed WeaveFFI 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 readI16(): Short { - val at = take(2) - return ((buf[at].toInt() and 0xFF) or ((buf[at + 1].toInt() and 0xFF) shl 8)).toShort() - } - 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(): Long = readI32().toLong() and 0xFFFFFFFFL - fun readI64(): Long { - val lo = readI32().toLong() and 0xFFFFFFFFL - val hi = readI32().toLong() - return lo or (hi shl 32) - } - 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) - } - fun readOptional(read: () -> T): T? = if (readBool()) read() else null - fun readList(read: () -> T): List { - val n = readLen() - val out = ArrayList(n) - repeat(n) { out.add(read()) } - return out - } - fun readMap(readKey: () -> K, readValue: () -> V): Map { - val n = readLen() - val out = LinkedHashMap() - repeat(n) { out[readKey()] = readValue() } - return out - } - fun expectEnd() { - if (pos != buf.size) malformed("trailing bytes after value") - } -} - -internal fun weaveEncode(write: (WeaveBufferWriter) -> Unit): ByteArray { - val w = WeaveBufferWriter() - write(w) - return w.toByteArray() -} - -internal fun weaveDecode(bytes: ByteArray, read: (WeaveBufferReader) -> T): T { - val r = WeaveBufferReader(bytes) - val v = read(r) - r.expectEnd() - return v -} - -internal class WeaveContinuation( - private val cont: kotlinx.coroutines.CancellableContinuation, - private val mapError: (Int, String, ByteArray?) -> Throwable -) { - @Suppress("UNCHECKED_CAST") - fun onSuccess(result: Any?) { cont.resume(result as T) } - fun onError(code: Int, message: String, payload: ByteArray?) { cont.resumeWithException(mapError(code, message, payload)) } -} - -// END WeaveFFI.kt diff --git a/crates/weaveffi-cli/tests/snapshots/android_calculator__build_gradle.snap b/crates/weaveffi-cli/tests/snapshots/android_calculator__build_gradle.snap deleted file mode 100644 index 5cf5c482..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_calculator__build_gradle.snap +++ /dev/null @@ -1,31 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -plugins { - id 'com.android.library' - id 'org.jetbrains.kotlin.android' version '1.9.22' apply false -} - -android { - namespace 'com.weaveffi' - compileSdk 34 - defaultConfig { - minSdk 24 - externalNativeBuild { - cmake { - cppFlags "" - } - } - } - externalNativeBuild { - cmake { - path "src/main/cpp/CMakeLists.txt" - } - } -} - -// END build.gradle diff --git a/crates/weaveffi-cli/tests/snapshots/android_calculator__settings_gradle.snap b/crates/weaveffi-cli/tests/snapshots/android_calculator__settings_gradle.snap deleted file mode 100644 index f10a8b92..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_calculator__settings_gradle.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -rootProject.name = 'weaveffi' - -// END settings.gradle diff --git a/crates/weaveffi-cli/tests/snapshots/android_calculator__src_main_cpp_CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/android_calculator__src_main_cpp_CMakeLists_txt.snap deleted file mode 100644 index e2c5c99f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_calculator__src_main_cpp_CMakeLists_txt.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -cmake_minimum_required(VERSION 3.22) -project(weaveffi) -add_library(weaveffi SHARED weaveffi_jni.c) -target_include_directories(weaveffi PRIVATE ../../../../c) - -# END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/android_calculator__src_main_cpp_weaveffi_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/android_calculator__src_main_cpp_weaveffi_jni_c.snap deleted file mode 100644 index 47046182..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_calculator__src_main_cpp_weaveffi_jni_c.snap +++ /dev/null @@ -1,92 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include -#include -#include -#include -#include -#include "weaveffi.h" - -static void throw_weaveffi_error(JNIEnv* env, weaveffi_error* err) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jclass exClass = (*env)->FindClass(env, "com/weaveffi/WeaveFFIException"); - if (exClass != NULL) { - jmethodID ctor = (*env)->GetMethodID(env, exClass, "", "(ILjava/lang/String;)V"); - jstring jmsg = (*env)->NewStringUTF(env, msg); - jthrowable ex = (jthrowable)(*env)->NewObject(env, exClass, ctor, (jint)err->code, jmsg); - if (ex != NULL) { (*env)->Throw(env, ex); } - } - weaveffi_error_clear(err); -} - -static void throw_weaveffi_calculator_CalcError(JNIEnv* env, weaveffi_error* err) { - jclass exClass = (*env)->FindClass(env, "com/weaveffi/CalcException"); - jmethodID fromCode = exClass ? (*env)->GetStaticMethodID(env, exClass, "fromCode", "(ILjava/lang/String;[B)Lcom/weaveffi/WeaveFFIException;") : NULL; - if (fromCode == NULL) { - (*env)->ExceptionClear(env); - throw_weaveffi_error(env, err); - return; - } - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = (*env)->NewStringUTF(env, msg); - jbyteArray jpayload = NULL; - if (err->payload_ptr != NULL) { - jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); - if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } - } - jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, exClass, fromCode, (jint)err->code, jmsg, jpayload); - if (ex != NULL && !(*env)->ExceptionCheck(env)) { (*env)->Throw(env, ex); } - weaveffi_error_clear(err); -} - -JNIEXPORT jint JNICALL Java_com_weaveffi_WeaveFFI_add(JNIEnv* env, jclass clazz, jint a, jint b) { - weaveffi_error err = {0, NULL, NULL, 0}; - int32_t rv = weaveffi_calculator_add((int32_t)a, (int32_t)b, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jint) rv; -} - -JNIEXPORT jint JNICALL Java_com_weaveffi_WeaveFFI_mul(JNIEnv* env, jclass clazz, jint a, jint b) { - weaveffi_error err = {0, NULL, NULL, 0}; - int32_t rv = weaveffi_calculator_mul((int32_t)a, (int32_t)b, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jint) rv; -} - -JNIEXPORT jint JNICALL Java_com_weaveffi_WeaveFFI_div(JNIEnv* env, jclass clazz, jint a, jint b) { - weaveffi_error err = {0, NULL, NULL, 0}; - int32_t rv = weaveffi_calculator_div((int32_t)a, (int32_t)b, &err); - if (err.code != 0) { - throw_weaveffi_calculator_CalcError(env, &err); - return 0; - } - return (jint) rv; -} - -JNIEXPORT jstring JNICALL Java_com_weaveffi_WeaveFFI_echo(JNIEnv* env, jclass clazz, jstring s) { - weaveffi_error err = {0, NULL, NULL, 0}; - const char* s_chars = (*env)->GetStringUTFChars(env, s, NULL); - const char* rv = weaveffi_calculator_echo(s_chars, &err); - (*env)->ReleaseStringUTFChars(env, s, s_chars); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return NULL; - } - jstring out = rv ? (*env)->NewStringUTF(env, rv) : (*env)->NewStringUTF(env, ""); - weaveffi_free_string(rv); - return out; -} - - -// END weaveffi_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/android_calculator__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap b/crates/weaveffi-cli/tests/snapshots/android_calculator__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap deleted file mode 100644 index 8faec248..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_calculator__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap +++ /dev/null @@ -1,49 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -package com.weaveffi - -class WeaveFFI { - companion object { - init { System.loadLibrary("weaveffi") } - - /** - * Add two integers - */ - @JvmStatic external fun add(a: Int, b: Int): Int - /** - * Multiply two integers - */ - @JvmStatic external fun mul(a: Int, b: Int): Int - /** - * Divide two integers, failing on a zero divisor - */ - @JvmStatic external fun div(a: Int, b: Int): Int - /** - * Echo a string back - */ - @JvmStatic external fun echo(s: String): String - } -} - -/** Generic WeaveFFI failure: panics, marshalling errors, and unknown codes. */ -open class WeaveFFIException(val code: Int, message: String) : Exception(message) - -/** Typed error domain `CalcError` declared by module `calculator`. */ -sealed class CalcException(code: Int, message: String) : WeaveFFIException(code, message) { - class DivisionByZero(message: String = "division by zero") : CalcException(1, message) - - companion object { - /** Map a raw `CalcError` code and payload to the typed exception; unknown codes yield the generic [WeaveFFIException]. */ - @JvmStatic fun fromCode(code: Int, message: String, payload: ByteArray?): WeaveFFIException = when (code) { - 1 -> DivisionByZero(message) - else -> WeaveFFIException(code, message) - } - } -} - -// END WeaveFFI.kt diff --git a/crates/weaveffi-cli/tests/snapshots/android_contacts__build_gradle.snap b/crates/weaveffi-cli/tests/snapshots/android_contacts__build_gradle.snap deleted file mode 100644 index 5cf5c482..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_contacts__build_gradle.snap +++ /dev/null @@ -1,31 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -plugins { - id 'com.android.library' - id 'org.jetbrains.kotlin.android' version '1.9.22' apply false -} - -android { - namespace 'com.weaveffi' - compileSdk 34 - defaultConfig { - minSdk 24 - externalNativeBuild { - cmake { - cppFlags "" - } - } - } - externalNativeBuild { - cmake { - path "src/main/cpp/CMakeLists.txt" - } - } -} - -// END build.gradle diff --git a/crates/weaveffi-cli/tests/snapshots/android_contacts__settings_gradle.snap b/crates/weaveffi-cli/tests/snapshots/android_contacts__settings_gradle.snap deleted file mode 100644 index f10a8b92..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_contacts__settings_gradle.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -rootProject.name = 'weaveffi' - -// END settings.gradle diff --git a/crates/weaveffi-cli/tests/snapshots/android_contacts__src_main_cpp_CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/android_contacts__src_main_cpp_CMakeLists_txt.snap deleted file mode 100644 index e2c5c99f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_contacts__src_main_cpp_CMakeLists_txt.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -cmake_minimum_required(VERSION 3.22) -project(weaveffi) -add_library(weaveffi SHARED weaveffi_jni.c) -target_include_directories(weaveffi PRIVATE ../../../../c) - -# END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/android_contacts__src_main_cpp_weaveffi_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/android_contacts__src_main_cpp_weaveffi_jni_c.snap deleted file mode 100644 index 221de85f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_contacts__src_main_cpp_weaveffi_jni_c.snap +++ /dev/null @@ -1,131 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include -#include -#include -#include -#include -#include "weaveffi.h" - -static void throw_weaveffi_error(JNIEnv* env, weaveffi_error* err) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jclass exClass = (*env)->FindClass(env, "com/weaveffi/WeaveFFIException"); - if (exClass != NULL) { - jmethodID ctor = (*env)->GetMethodID(env, exClass, "", "(ILjava/lang/String;)V"); - jstring jmsg = (*env)->NewStringUTF(env, msg); - jthrowable ex = (jthrowable)(*env)->NewObject(env, exClass, ctor, (jint)err->code, jmsg); - if (ex != NULL) { (*env)->Throw(env, ex); } - } - weaveffi_error_clear(err); -} - -static void throw_weaveffi_contacts_ContactsError(JNIEnv* env, weaveffi_error* err) { - jclass exClass = (*env)->FindClass(env, "com/weaveffi/ContactsException"); - jmethodID fromCode = exClass ? (*env)->GetStaticMethodID(env, exClass, "fromCode", "(ILjava/lang/String;[B)Lcom/weaveffi/WeaveFFIException;") : NULL; - if (fromCode == NULL) { - (*env)->ExceptionClear(env); - throw_weaveffi_error(env, err); - return; - } - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = (*env)->NewStringUTF(env, msg); - jbyteArray jpayload = NULL; - if (err->payload_ptr != NULL) { - jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); - if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } - } - jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, exClass, fromCode, (jint)err->code, jmsg, jpayload); - if (ex != NULL && !(*env)->ExceptionCheck(env)) { (*env)->Throw(env, ex); } - weaveffi_error_clear(err); -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_ContactBook_nativeNew(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - void* rv = weaveffi_contacts_ContactBook_new(&err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jlong)(intptr_t) rv; -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_ContactBook_nativeAdd(JNIEnv* env, jclass clazz, jlong selfHandle, jstring first_name, jstring last_name, jbyteArray email, jint contact_type) { - weaveffi_error err = {0, NULL, NULL, 0}; - const char* first_name_chars = (*env)->GetStringUTFChars(env, first_name, NULL); - const char* last_name_chars = (*env)->GetStringUTFChars(env, last_name, NULL); - jbyte* email_elems = (*env)->GetByteArrayElements(env, email, NULL); - jsize email_len = (*env)->GetArrayLength(env, email); - size_t out_len = 0; - const uint8_t* rv = weaveffi_contacts_ContactBook_add((const weaveffi_contacts_ContactBook*)(intptr_t)selfHandle, first_name_chars, last_name_chars, (const uint8_t*)email_elems, (size_t)email_len, (int32_t)contact_type, &out_len, &err); - (*env)->ReleaseStringUTFChars(env, first_name, first_name_chars); - (*env)->ReleaseStringUTFChars(env, last_name, last_name_chars); - (*env)->ReleaseByteArrayElements(env, email, email_elems, JNI_ABORT); - if (err.code != 0) { - throw_weaveffi_contacts_ContactsError(env, &err); - return NULL; - } - jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len); - if (out && rv) { (*env)->SetByteArrayRegion(env, out, 0, (jsize)out_len, (const jbyte*)rv); } - weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len); - return out; -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_ContactBook_nativeGet(JNIEnv* env, jclass clazz, jlong selfHandle, jlong id) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_contacts_ContactBook_get((const weaveffi_contacts_ContactBook*)(intptr_t)selfHandle, (int64_t)id, &out_len, &err); - if (err.code != 0) { - throw_weaveffi_contacts_ContactsError(env, &err); - return NULL; - } - jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len); - if (out && rv) { (*env)->SetByteArrayRegion(env, out, 0, (jsize)out_len, (const jbyte*)rv); } - weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len); - return out; -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_ContactBook_nativeList(JNIEnv* env, jclass clazz, jlong selfHandle) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_contacts_ContactBook_list((const weaveffi_contacts_ContactBook*)(intptr_t)selfHandle, &out_len, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return NULL; - } - jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len); - if (out && rv) { (*env)->SetByteArrayRegion(env, out, 0, (jsize)out_len, (const jbyte*)rv); } - weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len); - return out; -} - -JNIEXPORT jboolean JNICALL Java_com_weaveffi_ContactBook_nativeRemove(JNIEnv* env, jclass clazz, jlong selfHandle, jlong id) { - weaveffi_error err = {0, NULL, NULL, 0}; - bool rv = weaveffi_contacts_ContactBook_remove((const weaveffi_contacts_ContactBook*)(intptr_t)selfHandle, (int64_t)id, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return JNI_FALSE; - } - return rv ? JNI_TRUE : JNI_FALSE; -} - -JNIEXPORT jint JNICALL Java_com_weaveffi_ContactBook_nativeCount(JNIEnv* env, jclass clazz, jlong selfHandle) { - weaveffi_error err = {0, NULL, NULL, 0}; - int32_t rv = weaveffi_contacts_ContactBook_count((const weaveffi_contacts_ContactBook*)(intptr_t)selfHandle, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jint) rv; -} - -JNIEXPORT void JNICALL Java_com_weaveffi_ContactBook_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) { - weaveffi_contacts_ContactBook_destroy((weaveffi_contacts_ContactBook*)(intptr_t)handle); -} - - -// END weaveffi_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/android_contacts__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap b/crates/weaveffi-cli/tests/snapshots/android_contacts__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap deleted file mode 100644 index 11a8adf7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_contacts__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap +++ /dev/null @@ -1,246 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -package com.weaveffi - -class WeaveFFI { - companion object { - init { System.loadLibrary("weaveffi") } - - } -} - -enum class ContactType(val value: Int) { - Personal(0), - Work(1), - Other(2); - - companion object { - fun fromValue(value: Int): ContactType = entries.first { it.value == value } - } -} - -data class Contact(val id: Long, val first_name: String, val last_name: String, val email: String?, val contact_type: ContactType) - -internal fun packContact(w: WeaveBufferWriter, v: Contact) { - w.writeI64(v.id) - w.writeString(v.first_name) - w.writeString(v.last_name) - w.writeOptional(v.email) { v0 -> w.writeString(v0) } - w.writeI32(v.contact_type.value) -} - -internal fun unpackContact(r: WeaveBufferReader): Contact = Contact( - r.readI64(), - r.readString(), - r.readString(), - r.readOptional { r.readString() }, - ContactType.fromValue(r.readI32()), -) - -/** An in-memory address book owning its contacts */ -class ContactBook internal constructor(internal var handle: Long) : java.io.Closeable { - companion object { - init { System.loadLibrary("weaveffi") } - - @JvmStatic private external fun nativeNew(): Long - @JvmStatic private external fun nativeAdd(selfHandle: Long, firstName: String, lastName: String, email: ByteArray, contactType: Int): ByteArray - @JvmStatic private external fun nativeGet(selfHandle: Long, id: Long): ByteArray - @JvmStatic private external fun nativeList(selfHandle: Long): ByteArray - @JvmStatic private external fun nativeRemove(selfHandle: Long, id: Long): Boolean - @JvmStatic private external fun nativeCount(selfHandle: Long): Int - @JvmStatic private external fun nativeDestroy(handle: Long) - - operator fun invoke(): ContactBook = ContactBook(nativeNew()) - } - - fun add(firstName: String, lastName: String, email: String?, contactType: ContactType): Contact = weaveDecode(nativeAdd(handle, firstName, lastName, weaveEncode { w -> w.writeOptional(email) { v0 -> w.writeString(v0) } }, contactType.value)) { r -> unpackContact(r) } - - fun get(id: Long): Contact = weaveDecode(nativeGet(handle, id)) { r -> unpackContact(r) } - - fun list(): List = weaveDecode(nativeList(handle)) { r -> r.readList { unpackContact(r) } } - - fun remove(id: Long): Boolean = nativeRemove(handle, id) - - fun count(): Int = nativeCount(handle) - - override fun close() { - if (handle != 0L) { - nativeDestroy(handle) - handle = 0L - } - } - - protected fun finalize() { - close() - } -} - -/** Generic WeaveFFI failure: panics, marshalling errors, and unknown codes. */ -open class WeaveFFIException(val code: Int, message: String) : Exception(message) - -/** Typed error domain `ContactsError` declared by module `contacts`. */ -sealed class ContactsException(code: Int, message: String) : WeaveFFIException(code, message) { - class InvalidName(message: String = "name must not be empty") : ContactsException(1, message) - class NotFound(message: String = "contact not found") : ContactsException(2, message) - - companion object { - /** Map a raw `ContactsError` code and payload to the typed exception; unknown codes yield the generic [WeaveFFIException]. */ - @JvmStatic fun fromCode(code: Int, message: String, payload: ByteArray?): WeaveFFIException = when (code) { - 1 -> InvalidName(message) - 2 -> NotFound(message) - else -> WeaveFFIException(code, message) - } - } -} - -/** Growable little-endian writer implementing the WeaveFFI value-buffer wire format. */ -internal class WeaveBufferWriter { - private var buf = ByteArray(32) - private var len = 0 - - 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 writeI16(v: Short) { - reserve(2) - val b = v.toInt() - buf[len++] = (b and 0xFF).toByte() - buf[len++] = ((b shr 8) and 0xFF).toByte() - } - fun writeI32(v: Int) { - reserve(4) - buf[len++] = (v and 0xFF).toByte() - buf[len++] = ((v shr 8) and 0xFF).toByte() - buf[len++] = ((v shr 16) and 0xFF).toByte() - buf[len++] = ((v shr 24) and 0xFF).toByte() - } - fun writeU32(v: Long) = writeI32(v.toInt()) - fun writeI64(v: Long) { - writeI32(v.toInt()) - writeI32((v ushr 32).toInt()) - } - fun writeF32(v: Float) = writeI32(v.toRawBits()) - fun writeF64(v: Double) = writeI64(v.toRawBits()) - fun writeString(v: String) = writeBytes(v.toByteArray(Charsets.UTF_8)) - fun writeBytes(v: ByteArray) { - writeI32(v.size) - reserve(v.size) - v.copyInto(buf, len) - len += v.size - } - fun writeOptional(v: T?, write: (T) -> Unit) { - if (v == null) writeBool(false) else { writeBool(true); write(v) } - } - fun writeList(v: List, write: (T) -> Unit) { - writeI32(v.size) - for (e in v) write(e) - } - 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 WeaveFFI value-buffer wire format. */ -internal class WeaveBufferReader(private val buf: ByteArray) { - private var pos = 0 - - private fun malformed(detail: String): Nothing = - throw WeaveFFIException(-2, "malformed WeaveFFI 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 readI16(): Short { - val at = take(2) - return ((buf[at].toInt() and 0xFF) or ((buf[at + 1].toInt() and 0xFF) shl 8)).toShort() - } - 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(): Long = readI32().toLong() and 0xFFFFFFFFL - fun readI64(): Long { - val lo = readI32().toLong() and 0xFFFFFFFFL - val hi = readI32().toLong() - return lo or (hi shl 32) - } - 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) - } - fun readOptional(read: () -> T): T? = if (readBool()) read() else null - fun readList(read: () -> T): List { - val n = readLen() - val out = ArrayList(n) - repeat(n) { out.add(read()) } - return out - } - fun readMap(readKey: () -> K, readValue: () -> V): Map { - val n = readLen() - val out = LinkedHashMap() - repeat(n) { out[readKey()] = readValue() } - return out - } - fun expectEnd() { - if (pos != buf.size) malformed("trailing bytes after value") - } -} - -internal fun weaveEncode(write: (WeaveBufferWriter) -> Unit): ByteArray { - val w = WeaveBufferWriter() - write(w) - return w.toByteArray() -} - -internal fun weaveDecode(bytes: ByteArray, read: (WeaveBufferReader) -> T): T { - val r = WeaveBufferReader(bytes) - val v = read(r) - r.expectEnd() - return v -} - -// END WeaveFFI.kt diff --git a/crates/weaveffi-cli/tests/snapshots/android_async_demo__build_gradle.snap b/crates/weaveffi-cli/tests/snapshots/android_edge_cases__build_gradle.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/android_async_demo__build_gradle.snap rename to crates/weaveffi-cli/tests/snapshots/android_edge_cases__build_gradle.snap diff --git a/crates/weaveffi-cli/tests/snapshots/android_async_demo__settings_gradle.snap b/crates/weaveffi-cli/tests/snapshots/android_edge_cases__settings_gradle.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/android_async_demo__settings_gradle.snap rename to crates/weaveffi-cli/tests/snapshots/android_edge_cases__settings_gradle.snap diff --git a/crates/weaveffi-cli/tests/snapshots/android_async_demo__src_main_cpp_CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/android_edge_cases__src_main_cpp_CMakeLists_txt.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/android_async_demo__src_main_cpp_CMakeLists_txt.snap rename to crates/weaveffi-cli/tests/snapshots/android_edge_cases__src_main_cpp_CMakeLists_txt.snap diff --git a/crates/weaveffi-cli/tests/snapshots/android_edge_cases__src_main_cpp_weaveffi_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/android_edge_cases__src_main_cpp_weaveffi_jni_c.snap new file mode 100644 index 00000000..8c17d68f --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/android_edge_cases__src_main_cpp_weaveffi_jni_c.snap @@ -0,0 +1,900 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from weaveffi.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate weaveffi.yml -o + +#include +#include +#include +#include +#include +#include +#include "weaveffi.h" + +static void throw_weaveffi_error(JNIEnv* env, weaveffi_error* err) { + const char* msg = err->message ? err->message : "WeaveFFI error"; + jclass exClass = (*env)->FindClass(env, "com/weaveffi/WeaveFFIException"); + if (exClass != NULL) { + jmethodID ctor = (*env)->GetMethodID(env, exClass, "", "(ILjava/lang/String;)V"); + jstring jmsg = (*env)->NewStringUTF(env, msg); + jthrowable ex = (jthrowable)(*env)->NewObject(env, exClass, ctor, (jint)err->code, jmsg); + if (ex != NULL) { (*env)->Throw(env, ex); } + } + weaveffi_error_clear(err); +} + +static void throw_weaveffi_edge_cases_EdgeErrors(JNIEnv* env, weaveffi_error* err) { + jclass exClass = (*env)->FindClass(env, "com/weaveffi/EdgeErrorsException"); + jmethodID fromCode = exClass ? (*env)->GetStaticMethodID(env, exClass, "fromCode", "(ILjava/lang/String;[B)Lcom/weaveffi/WeaveFFIException;") : NULL; + if (fromCode == NULL) { + (*env)->ExceptionClear(env); + throw_weaveffi_error(env, err); + return; + } + const char* msg = err->message ? err->message : "WeaveFFI error"; + jstring jmsg = (*env)->NewStringUTF(env, msg); + jbyteArray jpayload = NULL; + if (err->payload_ptr != NULL) { + jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); + if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } + } + jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, exClass, fromCode, (jint)err->code, jmsg, jpayload); + if (ex != NULL && !(*env)->ExceptionCheck(env)) { (*env)->Throw(env, ex); } + weaveffi_error_clear(err); +} + +typedef struct { + JavaVM* jvm; + jobject callback; +} weaveffi_jni_async_ctx; + +static jclass weaveffi_jni_entry_class = NULL; +static jmethodID weaveffi_jni_dispatch_exc = NULL; + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { + (void)reserved; + JNIEnv* env = NULL; + if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } + jclass cls = (*env)->FindClass(env, "com/weaveffi/WeaveFFI"); + if (cls == NULL) { (*env)->ExceptionClear(env); return JNI_VERSION_1_6; } + weaveffi_jni_entry_class = (jclass)(*env)->NewGlobalRef(env, cls); + weaveffi_jni_dispatch_exc = (*env)->GetStaticMethodID(env, weaveffi_jni_entry_class, "dispatchCallbackException", "(Ljava/lang/Throwable;)V"); + if (weaveffi_jni_dispatch_exc == NULL) { (*env)->ExceptionClear(env); } + return JNI_VERSION_1_6; +} + +static void weaveffi_jni_handle_uncaught(JNIEnv* env) { + if (!(*env)->ExceptionCheck(env)) { return; } + jthrowable ex = (*env)->ExceptionOccurred(env); + (*env)->ExceptionClear(env); + if (weaveffi_jni_entry_class != NULL && weaveffi_jni_dispatch_exc != NULL) { + (*env)->CallStaticVoidMethod(env, weaveffi_jni_entry_class, weaveffi_jni_dispatch_exc, ex); + if (!(*env)->ExceptionCheck(env)) { (*env)->DeleteLocalRef(env, ex); return; } + (*env)->ExceptionClear(env); + } + (*env)->Throw(env, ex); + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + (*env)->DeleteLocalRef(env, ex); +} + +typedef struct weaveffi_jni_listener_ctx { + JavaVM* jvm; + jobject callback; + uint64_t id; + struct weaveffi_jni_listener_ctx* next; +} weaveffi_jni_listener_ctx; + +static weaveffi_jni_listener_ctx* weaveffi_jni_listeners = NULL; +static pthread_mutex_t weaveffi_jni_listener_lock = PTHREAD_MUTEX_INITIALIZER; + +static void weaveffi_edge_cases_OnBatch_fn_jni_tramp(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, weaveffi_edge_cases_Mode mode, void* context) { + weaveffi_jni_listener_ctx* ctx = (weaveffi_jni_listener_ctx*)context; + JNIEnv* env = NULL; + int attached = 0; + if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) return; + attached = 1; + } + if ((*env)->PushLocalFrame(env, 32) != 0) { + if (attached) (*ctx->jvm)->DetachCurrentThread(ctx->jvm); + return; + } + jbyteArray _a0 = (*env)->NewByteArray(env, (jsize)items_len); + if (_a0 && items_ptr) { (*env)->SetByteArrayRegion(env, _a0, 0, (jsize)items_len, (const jbyte*)items_ptr); } + jbyteArray _a1 = (*env)->NewByteArray(env, (jsize)raw_len); + if (_a1 && raw_ptr) { (*env)->SetByteArrayRegion(env, _a1, 0, (jsize)raw_len, (const jbyte*)raw_ptr); } + jbyteArray _a2 = (*env)->NewByteArray(env, (jsize)first_len); + if (_a2 && first_ptr) { (*env)->SetByteArrayRegion(env, _a2, 0, (jsize)first_len, (const jbyte*)first_ptr); } + jclass _a3_cls = (*env)->FindClass(env, "java/lang/Integer"); + jobject _a3 = (*env)->CallStaticObjectMethod(env, _a3_cls, (*env)->GetStaticMethodID(env, _a3_cls, "valueOf", "(I)Ljava/lang/Integer;"), (jint)mode); + jclass fn_cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID invoke = (*env)->GetMethodID(env, fn_cls, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); + (*env)->CallObjectMethod(env, ctx->callback, invoke, _a0, _a1, _a2, _a3); + weaveffi_jni_handle_uncaught(env); + (*env)->PopLocalFrame(env, NULL); + if (attached) (*ctx->jvm)->DetachCurrentThread(ctx->jvm); +} + +static void weaveffi_edge_cases_OnTick_fn_jni_tramp(void* context) { + weaveffi_jni_listener_ctx* ctx = (weaveffi_jni_listener_ctx*)context; + JNIEnv* env = NULL; + int attached = 0; + if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) return; + attached = 1; + } + if ((*env)->PushLocalFrame(env, 32) != 0) { + if (attached) (*ctx->jvm)->DetachCurrentThread(ctx->jvm); + return; + } + jclass fn_cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID invoke = (*env)->GetMethodID(env, fn_cls, "invoke", "()Ljava/lang/Object;"); + (*env)->CallObjectMethod(env, ctx->callback, invoke); + weaveffi_jni_handle_uncaught(env); + (*env)->PopLocalFrame(env, NULL); + if (attached) (*ctx->jvm)->DetachCurrentThread(ctx->jvm); +} + +JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_registerBatchListenerJni(JNIEnv* env, jclass clazz, jobject callback) { + weaveffi_jni_listener_ctx* ctx = (weaveffi_jni_listener_ctx*)calloc(1, sizeof(weaveffi_jni_listener_ctx)); + (*env)->GetJavaVM(env, &ctx->jvm); + ctx->callback = (*env)->NewGlobalRef(env, callback); + uint64_t id = weaveffi_edge_cases_register_batch_listener(weaveffi_edge_cases_OnBatch_fn_jni_tramp, ctx); + ctx->id = id; + pthread_mutex_lock(&weaveffi_jni_listener_lock); + ctx->next = weaveffi_jni_listeners; + weaveffi_jni_listeners = ctx; + pthread_mutex_unlock(&weaveffi_jni_listener_lock); + return (jlong)id; +} + +JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_unregisterBatchListener(JNIEnv* env, jclass clazz, jlong id) { + weaveffi_edge_cases_unregister_batch_listener((uint64_t)id); + pthread_mutex_lock(&weaveffi_jni_listener_lock); + weaveffi_jni_listener_ctx** link = &weaveffi_jni_listeners; + while (*link != NULL) { + if ((*link)->id == (uint64_t)id) { + weaveffi_jni_listener_ctx* found = *link; + *link = found->next; + (*env)->DeleteGlobalRef(env, found->callback); + free(found); + break; + } + link = &(*link)->next; + } + pthread_mutex_unlock(&weaveffi_jni_listener_lock); +} + +JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_registerTickListener(JNIEnv* env, jclass clazz, jobject callback) { + weaveffi_jni_listener_ctx* ctx = (weaveffi_jni_listener_ctx*)calloc(1, sizeof(weaveffi_jni_listener_ctx)); + (*env)->GetJavaVM(env, &ctx->jvm); + ctx->callback = (*env)->NewGlobalRef(env, callback); + uint64_t id = weaveffi_edge_cases_register_tick_listener(weaveffi_edge_cases_OnTick_fn_jni_tramp, ctx); + ctx->id = id; + pthread_mutex_lock(&weaveffi_jni_listener_lock); + ctx->next = weaveffi_jni_listeners; + weaveffi_jni_listeners = ctx; + pthread_mutex_unlock(&weaveffi_jni_listener_lock); + return (jlong)id; +} + +JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_unregisterTickListener(JNIEnv* env, jclass clazz, jlong id) { + weaveffi_edge_cases_unregister_tick_listener((uint64_t)id); + pthread_mutex_lock(&weaveffi_jni_listener_lock); + weaveffi_jni_listener_ctx** link = &weaveffi_jni_listeners; + while (*link != NULL) { + if ((*link)->id == (uint64_t)id) { + weaveffi_jni_listener_ctx* found = *link; + *link = found->next; + (*env)->DeleteGlobalRef(env, found->callback); + free(found); + break; + } + link = &(*link)->next; + } + pthread_mutex_unlock(&weaveffi_jni_listener_lock); +} + +JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_noop(JNIEnv* env, jclass clazz) { + weaveffi_error err = {0, NULL, NULL, 0}; + weaveffi_edge_cases_noop(&err); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + + } + return; +} + +JNIEXPORT jint JNICALL Java_com_weaveffi_WeaveFFI_selectJni(JNIEnv* env, jclass clazz, jint class, jstring from, jbyteArray func, jbyteArray range, jboolean type_) { + weaveffi_error err = {0, NULL, NULL, 0}; + const char* from_chars = (*env)->GetStringUTFChars(env, from, NULL); + jbyte* func_elems = (*env)->GetByteArrayElements(env, func, NULL); + jsize func_len = (*env)->GetArrayLength(env, func); + jbyte* range_elems = (*env)->GetByteArrayElements(env, range, NULL); + jsize range_len = (*env)->GetArrayLength(env, range); + int32_t rv = weaveffi_edge_cases_select((int32_t)class, from_chars, (const uint8_t*)func_elems, (size_t)func_len, (const uint8_t*)range_elems, (size_t)range_len, (bool)(type_ == JNI_TRUE), &err); + (*env)->ReleaseStringUTFChars(env, from, from_chars); + (*env)->ReleaseByteArrayElements(env, func, func_elems, JNI_ABORT); + (*env)->ReleaseByteArrayElements(env, range, range_elems, JNI_ABORT); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return 0; + } + return (jint) rv; +} + +JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_nestedOptionalJni(JNIEnv* env, jclass clazz, jbyteArray items) { + weaveffi_error err = {0, NULL, NULL, 0}; + jbyte* items_elems = (*env)->GetByteArrayElements(env, items, NULL); + jsize items_len = (*env)->GetArrayLength(env, items); + size_t out_len = 0; + const uint8_t* rv = weaveffi_edge_cases_nested_optional((const uint8_t*)items_elems, (size_t)items_len, &out_len, &err); + (*env)->ReleaseByteArrayElements(env, items, items_elems, JNI_ABORT); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return NULL; + } + jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len); + if (out && rv) { (*env)->SetByteArrayRegion(env, out, 0, (jsize)out_len, (const jbyte*)rv); } + weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len); + return out; +} + +JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_optionalParamsJni(JNIEnv* env, jclass clazz, jbyteArray text, jbyteArray count, jbyteArray outer, jbyteArray tags, jbyteArray blob, jbyteArray mode) { + weaveffi_error err = {0, NULL, NULL, 0}; + jbyte* text_elems = (*env)->GetByteArrayElements(env, text, NULL); + jsize text_len = (*env)->GetArrayLength(env, text); + jbyte* count_elems = (*env)->GetByteArrayElements(env, count, NULL); + jsize count_len = (*env)->GetArrayLength(env, count); + jbyte* outer_elems = (*env)->GetByteArrayElements(env, outer, NULL); + jsize outer_len = (*env)->GetArrayLength(env, outer); + jbyte* tags_elems = (*env)->GetByteArrayElements(env, tags, NULL); + jsize tags_len = (*env)->GetArrayLength(env, tags); + jbyte* blob_elems = (*env)->GetByteArrayElements(env, blob, NULL); + jsize blob_len = (*env)->GetArrayLength(env, blob); + jbyte* mode_elems = (*env)->GetByteArrayElements(env, mode, NULL); + jsize mode_len = (*env)->GetArrayLength(env, mode); + size_t out_len = 0; + const uint8_t* rv = weaveffi_edge_cases_optional_params((const uint8_t*)text_elems, (size_t)text_len, (const uint8_t*)count_elems, (size_t)count_len, (const uint8_t*)outer_elems, (size_t)outer_len, (const uint8_t*)tags_elems, (size_t)tags_len, (const uint8_t*)blob_elems, (size_t)blob_len, (const uint8_t*)mode_elems, (size_t)mode_len, &out_len, &err); + (*env)->ReleaseByteArrayElements(env, text, text_elems, JNI_ABORT); + (*env)->ReleaseByteArrayElements(env, count, count_elems, JNI_ABORT); + (*env)->ReleaseByteArrayElements(env, outer, outer_elems, JNI_ABORT); + (*env)->ReleaseByteArrayElements(env, tags, tags_elems, JNI_ABORT); + (*env)->ReleaseByteArrayElements(env, blob, blob_elems, JNI_ABORT); + (*env)->ReleaseByteArrayElements(env, mode, mode_elems, JNI_ABORT); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return NULL; + } + jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len); + if (out && rv) { (*env)->SetByteArrayRegion(env, out, 0, (jsize)out_len, (const jbyte*)rv); } + weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len); + return out; +} + +JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_mutableParams(JNIEnv* env, jclass clazz, jstring text, jbyteArray blob) { + weaveffi_error err = {0, NULL, NULL, 0}; + const char* text_chars = (*env)->GetStringUTFChars(env, text, NULL); + jboolean blob_is_copy = 0; + jbyte* blob_elems = (*env)->GetByteArrayElements(env, blob, &blob_is_copy); + jsize blob_len = (*env)->GetArrayLength(env, blob); + weaveffi_edge_cases_mutable_params(text_chars, (const uint8_t*)blob_elems, (size_t)blob_len, &err); + (*env)->ReleaseStringUTFChars(env, text, text_chars); + (*env)->ReleaseByteArrayElements(env, blob, blob_elems, 0); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + + } + return; +} + +JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_makeWidgetJni(JNIEnv* env, jclass clazz, jstring label) { + weaveffi_error err = {0, NULL, NULL, 0}; + const char* label_chars = (*env)->GetStringUTFChars(env, label, NULL); + void* rv = weaveffi_edge_cases_make_widget(label_chars, &err); + (*env)->ReleaseStringUTFChars(env, label, label_chars); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return 0; + } + return (jlong)(intptr_t) rv; +} + +JNIEXPORT jobject JNICALL Java_com_weaveffi_WeaveFFI_findWidgetJni(JNIEnv* env, jclass clazz, jlong id) { + weaveffi_error err = {0, NULL, NULL, 0}; + weaveffi_edge_cases_Widget* rv = weaveffi_edge_cases_find_widget((int64_t)id, &err); + if (err.code != 0) { + throw_weaveffi_edge_cases_EdgeErrors(env, &err); + return NULL; + } + if (rv == NULL) { return NULL; } + jclass box_cls = (*env)->FindClass(env, "java/lang/Long"); + jmethodID box_mid = (*env)->GetStaticMethodID(env, box_cls, "valueOf", "(J)Ljava/lang/Long;"); + return (*env)->CallStaticObjectMethod(env, box_cls, box_mid, (jlong)(intptr_t)rv); +} + +JNIEXPORT jstring JNICALL Java_com_weaveffi_WeaveFFI_inspectWidgetJni(JNIEnv* env, jclass clazz, jlong widget, jobject fallback) { + weaveffi_error err = {0, NULL, NULL, 0}; + int64_t fallback_val = 0; + if (fallback != NULL) { + jclass fallback_cls = (*env)->FindClass(env, "java/lang/Long"); + jmethodID fallback_mid = (*env)->GetMethodID(env, fallback_cls, "longValue", "()J"); + fallback_val = (int64_t)(*env)->CallLongMethod(env, fallback, fallback_mid); + } + const char* rv = weaveffi_edge_cases_inspect_widget((const weaveffi_edge_cases_Widget*)(intptr_t)widget, (const weaveffi_edge_cases_Widget*)(intptr_t)fallback_val, &err); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return NULL; + } + jstring out = rv ? (*env)->NewStringUTF(env, rv) : (*env)->NewStringUTF(env, ""); + weaveffi_free_string(rv); + return out; +} + +JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_typedHandleParam(JNIEnv* env, jclass clazz, jlong token, jlong raw) { + weaveffi_error err = {0, NULL, NULL, 0}; + weaveffi_edge_cases_Leaf* rv = weaveffi_edge_cases_typed_handle_param((weaveffi_edge_cases_Leaf*)(intptr_t)token, (weaveffi_handle_t)raw, &err); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return 0; + } + return (jlong)(intptr_t)rv; +} + +JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_stringStreamJni(JNIEnv* env, jclass clazz) { + weaveffi_error err = {0, NULL, NULL, 0}; + weaveffi_edge_cases_StringStreamIterator* _iter = weaveffi_edge_cases_string_stream(&err); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return 0; + } + return (jlong)(intptr_t)_iter; +} + +JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_scalarStreamJni(JNIEnv* env, jclass clazz) { + weaveffi_error err = {0, NULL, NULL, 0}; + weaveffi_edge_cases_ScalarStreamIterator* _iter = weaveffi_edge_cases_scalar_stream(&err); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return 0; + } + return (jlong)(intptr_t)_iter; +} + +JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_modeStreamJni(JNIEnv* env, jclass clazz) { + weaveffi_error err = {0, NULL, NULL, 0}; + weaveffi_edge_cases_ModeStreamIterator* _iter = weaveffi_edge_cases_mode_stream(&err); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return 0; + } + return (jlong)(intptr_t)_iter; +} + +static void weaveffi_edge_cases_async_void_jni_cb(void* context, weaveffi_error* err) { + weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)context; + JNIEnv* env = NULL; + int attached = 0; + if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) { free(ctx); return; } + attached = 1; + } + if (err != NULL && err->code != 0) { + const char* msg = err->message ? err->message : "WeaveFFI error"; + jstring jmsg = (*env)->NewStringUTF(env, msg); + jbyteArray jpayload = NULL; + if (err->payload_ptr != NULL) { + jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); + if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } + } + jint jcode = (jint)err->code; + weaveffi_error_free(err); + jclass cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID mid = (*env)->GetMethodID(env, cls, "onError", "(ILjava/lang/String;[B)V"); + (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); + } else { + jobject boxed = NULL; + jclass cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID mid = (*env)->GetMethodID(env, cls, "onSuccess", "(Ljava/lang/Object;)V"); + (*env)->CallVoidMethod(env, ctx->callback, mid, boxed); + } + weaveffi_jni_handle_uncaught(env); + (*env)->DeleteGlobalRef(env, ctx->callback); + JavaVM* jvm = ctx->jvm; + free(ctx); + if (attached) (*jvm)->DetachCurrentThread(jvm); +} + +JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_asyncVoidAsync(JNIEnv* env, jclass clazz, jobject callback) { + weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx)); + (*env)->GetJavaVM(env, &ctx->jvm); + ctx->callback = (*env)->NewGlobalRef(env, callback); + weaveffi_edge_cases_async_void_async(weaveffi_edge_cases_async_void_jni_cb, ctx); +} + +static void weaveffi_edge_cases_async_bytes_jni_cb(void* context, weaveffi_error* err, const uint8_t* result, size_t result_len) { + weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)context; + JNIEnv* env = NULL; + int attached = 0; + if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) { free(ctx); return; } + attached = 1; + } + if (err != NULL && err->code != 0) { + const char* msg = err->message ? err->message : "WeaveFFI error"; + jstring jmsg = (*env)->NewStringUTF(env, msg); + jbyteArray jpayload = NULL; + if (err->payload_ptr != NULL) { + jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); + if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } + } + jint jcode = (jint)err->code; + weaveffi_error_free(err); + jclass cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID mid = (*env)->GetMethodID(env, cls, "onError", "(ILjava/lang/String;[B)V"); + (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); + } else { + jbyteArray boxed = (*env)->NewByteArray(env, (jsize)result_len); + if (boxed && result) { (*env)->SetByteArrayRegion(env, boxed, 0, (jsize)result_len, (const jbyte*)result); } + weaveffi_free_bytes((uint8_t*)result, result_len); + jclass cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID mid = (*env)->GetMethodID(env, cls, "onSuccess", "(Ljava/lang/Object;)V"); + (*env)->CallVoidMethod(env, ctx->callback, mid, boxed); + } + weaveffi_jni_handle_uncaught(env); + (*env)->DeleteGlobalRef(env, ctx->callback); + JavaVM* jvm = ctx->jvm; + free(ctx); + if (attached) (*jvm)->DetachCurrentThread(jvm); +} + +JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_asyncBytesAsync(JNIEnv* env, jclass clazz, jlong size, jobject callback) { + weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx)); + (*env)->GetJavaVM(env, &ctx->jvm); + ctx->callback = (*env)->NewGlobalRef(env, callback); + weaveffi_edge_cases_async_bytes_async((uint32_t)size, weaveffi_edge_cases_async_bytes_jni_cb, ctx); +} + +static void weaveffi_edge_cases_async_struct_jni_cb(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { + weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)context; + JNIEnv* env = NULL; + int attached = 0; + if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) { free(ctx); return; } + attached = 1; + } + if (err != NULL && err->code != 0) { + const char* msg = err->message ? err->message : "WeaveFFI error"; + jstring jmsg = (*env)->NewStringUTF(env, msg); + jbyteArray jpayload = NULL; + if (err->payload_ptr != NULL) { + jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); + if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } + } + jint jcode = (jint)err->code; + weaveffi_error_free(err); + jclass cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID mid = (*env)->GetMethodID(env, cls, "onError", "(ILjava/lang/String;[B)V"); + (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); + } else { + jbyteArray boxed = (*env)->NewByteArray(env, (jsize)result_len); + if (boxed && result_ptr) { (*env)->SetByteArrayRegion(env, boxed, 0, (jsize)result_len, (const jbyte*)result_ptr); } + weaveffi_free_bytes((uint8_t*)result_ptr, result_len); + jclass cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID mid = (*env)->GetMethodID(env, cls, "onSuccess", "(Ljava/lang/Object;)V"); + (*env)->CallVoidMethod(env, ctx->callback, mid, boxed); + } + weaveffi_jni_handle_uncaught(env); + (*env)->DeleteGlobalRef(env, ctx->callback); + JavaVM* jvm = ctx->jvm; + free(ctx); + if (attached) (*jvm)->DetachCurrentThread(jvm); +} + +JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_asyncStructAsync(JNIEnv* env, jclass clazz, jobject callback) { + weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx)); + (*env)->GetJavaVM(env, &ctx->jvm); + ctx->callback = (*env)->NewGlobalRef(env, callback); + weaveffi_edge_cases_async_struct_async(weaveffi_edge_cases_async_struct_jni_cb, ctx); +} + +static void weaveffi_edge_cases_async_list_jni_cb(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { + weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)context; + JNIEnv* env = NULL; + int attached = 0; + if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) { free(ctx); return; } + attached = 1; + } + if (err != NULL && err->code != 0) { + const char* msg = err->message ? err->message : "WeaveFFI error"; + jstring jmsg = (*env)->NewStringUTF(env, msg); + jbyteArray jpayload = NULL; + if (err->payload_ptr != NULL) { + jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); + if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } + } + jint jcode = (jint)err->code; + weaveffi_error_free(err); + jclass cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID mid = (*env)->GetMethodID(env, cls, "onError", "(ILjava/lang/String;[B)V"); + (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); + } else { + jbyteArray boxed = (*env)->NewByteArray(env, (jsize)result_len); + if (boxed && result_ptr) { (*env)->SetByteArrayRegion(env, boxed, 0, (jsize)result_len, (const jbyte*)result_ptr); } + weaveffi_free_bytes((uint8_t*)result_ptr, result_len); + jclass cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID mid = (*env)->GetMethodID(env, cls, "onSuccess", "(Ljava/lang/Object;)V"); + (*env)->CallVoidMethod(env, ctx->callback, mid, boxed); + } + weaveffi_jni_handle_uncaught(env); + (*env)->DeleteGlobalRef(env, ctx->callback); + JavaVM* jvm = ctx->jvm; + free(ctx); + if (attached) (*jvm)->DetachCurrentThread(jvm); +} + +JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_asyncListAsync(JNIEnv* env, jclass clazz, jlong cancelToken, jobject callback) { + weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx)); + (*env)->GetJavaVM(env, &ctx->jvm); + ctx->callback = (*env)->NewGlobalRef(env, callback); + weaveffi_edge_cases_async_list_async((weaveffi_cancel_token*)(intptr_t)cancelToken, weaveffi_edge_cases_async_list_jni_cb, ctx); +} + +static void weaveffi_edge_cases_async_optional_jni_cb(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { + weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)context; + JNIEnv* env = NULL; + int attached = 0; + if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) { free(ctx); return; } + attached = 1; + } + if (err != NULL && err->code != 0) { + const char* msg = err->message ? err->message : "WeaveFFI error"; + jstring jmsg = (*env)->NewStringUTF(env, msg); + jbyteArray jpayload = NULL; + if (err->payload_ptr != NULL) { + jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); + if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } + } + jint jcode = (jint)err->code; + weaveffi_error_free(err); + jclass cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID mid = (*env)->GetMethodID(env, cls, "onError", "(ILjava/lang/String;[B)V"); + (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); + } else { + jbyteArray boxed = (*env)->NewByteArray(env, (jsize)result_len); + if (boxed && result_ptr) { (*env)->SetByteArrayRegion(env, boxed, 0, (jsize)result_len, (const jbyte*)result_ptr); } + weaveffi_free_bytes((uint8_t*)result_ptr, result_len); + jclass cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID mid = (*env)->GetMethodID(env, cls, "onSuccess", "(Ljava/lang/Object;)V"); + (*env)->CallVoidMethod(env, ctx->callback, mid, boxed); + } + weaveffi_jni_handle_uncaught(env); + (*env)->DeleteGlobalRef(env, ctx->callback); + JavaVM* jvm = ctx->jvm; + free(ctx); + if (attached) (*jvm)->DetachCurrentThread(jvm); +} + +JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_asyncOptionalAsync(JNIEnv* env, jclass clazz, jobject callback) { + weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx)); + (*env)->GetJavaVM(env, &ctx->jvm); + ctx->callback = (*env)->NewGlobalRef(env, callback); + weaveffi_edge_cases_async_optional_async(weaveffi_edge_cases_async_optional_jni_cb, ctx); +} + +static void weaveffi_edge_cases_async_enum_jni_cb(void* context, weaveffi_error* err, weaveffi_edge_cases_Mode result) { + weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)context; + JNIEnv* env = NULL; + int attached = 0; + if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) { free(ctx); return; } + attached = 1; + } + if (err != NULL && err->code != 0) { + const char* msg = err->message ? err->message : "WeaveFFI error"; + jstring jmsg = (*env)->NewStringUTF(env, msg); + jbyteArray jpayload = NULL; + if (err->payload_ptr != NULL) { + jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); + if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } + } + jint jcode = (jint)err->code; + weaveffi_error_free(err); + jclass cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID mid = (*env)->GetMethodID(env, cls, "onError", "(ILjava/lang/String;[B)V"); + (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); + } else { + jclass boxCls = (*env)->FindClass(env, "java/lang/Integer"); + jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(I)Ljava/lang/Integer;"); + jobject boxed = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, (jint)result); + jclass cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID mid = (*env)->GetMethodID(env, cls, "onSuccess", "(Ljava/lang/Object;)V"); + (*env)->CallVoidMethod(env, ctx->callback, mid, boxed); + } + weaveffi_jni_handle_uncaught(env); + (*env)->DeleteGlobalRef(env, ctx->callback); + JavaVM* jvm = ctx->jvm; + free(ctx); + if (attached) (*jvm)->DetachCurrentThread(jvm); +} + +JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_asyncEnumAsync(JNIEnv* env, jclass clazz, jobject callback) { + weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx)); + (*env)->GetJavaVM(env, &ctx->jvm); + ctx->callback = (*env)->NewGlobalRef(env, callback); + weaveffi_edge_cases_async_enum_async(weaveffi_edge_cases_async_enum_jni_cb, ctx); +} + +static void weaveffi_edge_cases_async_keywords_jni_cb(void* context, weaveffi_error* err, const char* result) { + weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)context; + JNIEnv* env = NULL; + int attached = 0; + if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) { free(ctx); return; } + attached = 1; + } + if (err != NULL && err->code != 0) { + const char* msg = err->message ? err->message : "WeaveFFI error"; + jstring jmsg = (*env)->NewStringUTF(env, msg); + jbyteArray jpayload = NULL; + if (err->payload_ptr != NULL) { + jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); + if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } + } + jint jcode = (jint)err->code; + weaveffi_error_free(err); + jclass cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID mid = (*env)->GetMethodID(env, cls, "onError", "(ILjava/lang/String;[B)V"); + (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); + } else { + jobject boxed = result ? (jobject)(*env)->NewStringUTF(env, result) : (jobject)(*env)->NewStringUTF(env, ""); + weaveffi_free_string(result); + jclass cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID mid = (*env)->GetMethodID(env, cls, "onSuccess", "(Ljava/lang/Object;)V"); + (*env)->CallVoidMethod(env, ctx->callback, mid, boxed); + } + weaveffi_jni_handle_uncaught(env); + (*env)->DeleteGlobalRef(env, ctx->callback); + JavaVM* jvm = ctx->jvm; + free(ctx); + if (attached) (*jvm)->DetachCurrentThread(jvm); +} + +JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_asyncKeywordsAsync(JNIEnv* env, jclass clazz, jstring class, jint import, jobject callback) { + weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx)); + (*env)->GetJavaVM(env, &ctx->jvm); + ctx->callback = (*env)->NewGlobalRef(env, callback); + const char* class_chars = (*env)->GetStringUTFChars(env, class, NULL); + weaveffi_edge_cases_async_keywords_async(class_chars, (int32_t)import, weaveffi_edge_cases_async_keywords_jni_cb, ctx); + (*env)->ReleaseStringUTFChars(env, class, class_chars); +} + +JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_keywordsRoundtripJni(JNIEnv* env, jclass clazz, jbyteArray value) { + weaveffi_error err = {0, NULL, NULL, 0}; + jbyte* value_elems = (*env)->GetByteArrayElements(env, value, NULL); + jsize value_len = (*env)->GetArrayLength(env, value); + size_t out_len = 0; + const uint8_t* rv = weaveffi_edge_cases_keywords_roundtrip((const uint8_t*)value_elems, (size_t)value_len, &out_len, &err); + (*env)->ReleaseByteArrayElements(env, value, value_elems, JNI_ABORT); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return NULL; + } + jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len); + if (out && rv) { (*env)->SetByteArrayRegion(env, out, 0, (jsize)out_len, (const jbyte*)rv); } + weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len); + return out; +} + +JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_legacyValueJni(JNIEnv* env, jclass clazz, jint value) { + weaveffi_error err = {0, NULL, NULL, 0}; + size_t out_len = 0; + const uint8_t* rv = weaveffi_edge_cases_legacy_value((int32_t)value, &out_len, &err); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return NULL; + } + jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len); + if (out && rv) { (*env)->SetByteArrayRegion(env, out, 0, (jsize)out_len, (const jbyte*)rv); } + weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len); + return out; +} + +JNIEXPORT void JNICALL Java_com_weaveffi_Widget_nativeRefresh(JNIEnv* env, jclass clazz, jlong selfHandle) { + weaveffi_error err = {0, NULL, NULL, 0}; + weaveffi_edge_cases_Widget_refresh((const weaveffi_edge_cases_Widget*)(intptr_t)selfHandle, &err); + if (err.code != 0) { + throw_weaveffi_edge_cases_EdgeErrors(env, &err); + + } + return; +} + +JNIEXPORT jstring JNICALL Java_com_weaveffi_Widget_nativeRename(JNIEnv* env, jclass clazz, jlong selfHandle, jstring class, jbyteArray import) { + weaveffi_error err = {0, NULL, NULL, 0}; + const char* class_chars = (*env)->GetStringUTFChars(env, class, NULL); + jbyte* import_elems = (*env)->GetByteArrayElements(env, import, NULL); + jsize import_len = (*env)->GetArrayLength(env, import); + const char* rv = weaveffi_edge_cases_Widget_rename((const weaveffi_edge_cases_Widget*)(intptr_t)selfHandle, class_chars, (const uint8_t*)import_elems, (size_t)import_len, &err); + (*env)->ReleaseStringUTFChars(env, class, class_chars); + (*env)->ReleaseByteArrayElements(env, import, import_elems, JNI_ABORT); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return NULL; + } + jstring out = rv ? (*env)->NewStringUTF(env, rv) : (*env)->NewStringUTF(env, ""); + weaveffi_free_string(rv); + return out; +} + +static void weaveffi_edge_cases_Widget_fetch_jni_cb(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { + weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)context; + JNIEnv* env = NULL; + int attached = 0; + if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) { free(ctx); return; } + attached = 1; + } + if (err != NULL && err->code != 0) { + const char* msg = err->message ? err->message : "WeaveFFI error"; + jstring jmsg = (*env)->NewStringUTF(env, msg); + jbyteArray jpayload = NULL; + if (err->payload_ptr != NULL) { + jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); + if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } + } + jint jcode = (jint)err->code; + weaveffi_error_free(err); + jclass cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID mid = (*env)->GetMethodID(env, cls, "onError", "(ILjava/lang/String;[B)V"); + (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); + } else { + jbyteArray boxed = (*env)->NewByteArray(env, (jsize)result_len); + if (boxed && result_ptr) { (*env)->SetByteArrayRegion(env, boxed, 0, (jsize)result_len, (const jbyte*)result_ptr); } + weaveffi_free_bytes((uint8_t*)result_ptr, result_len); + jclass cls = (*env)->GetObjectClass(env, ctx->callback); + jmethodID mid = (*env)->GetMethodID(env, cls, "onSuccess", "(Ljava/lang/Object;)V"); + (*env)->CallVoidMethod(env, ctx->callback, mid, boxed); + } + weaveffi_jni_handle_uncaught(env); + (*env)->DeleteGlobalRef(env, ctx->callback); + JavaVM* jvm = ctx->jvm; + free(ctx); + if (attached) (*jvm)->DetachCurrentThread(jvm); +} + +JNIEXPORT void JNICALL Java_com_weaveffi_Widget_nativeFetchAsync(JNIEnv* env, jclass clazz, jlong selfHandle, jlong id, jobject callback) { + weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx)); + (*env)->GetJavaVM(env, &ctx->jvm); + ctx->callback = (*env)->NewGlobalRef(env, callback); + weaveffi_edge_cases_Widget_fetch_async((const weaveffi_edge_cases_Widget*)(intptr_t)selfHandle, (int64_t)id, weaveffi_edge_cases_Widget_fetch_jni_cb, ctx); +} + +JNIEXPORT jstring JNICALL Java_com_weaveffi_Widget_nativeOldName(JNIEnv* env, jclass clazz, jlong selfHandle) { + weaveffi_error err = {0, NULL, NULL, 0}; + const char* rv = weaveffi_edge_cases_Widget_old_name((const weaveffi_edge_cases_Widget*)(intptr_t)selfHandle, &err); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return NULL; + } + jstring out = rv ? (*env)->NewStringUTF(env, rv) : (*env)->NewStringUTF(env, ""); + weaveffi_free_string(rv); + return out; +} + +JNIEXPORT void JNICALL Java_com_weaveffi_Widget_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) { + weaveffi_edge_cases_Widget_destroy((weaveffi_edge_cases_Widget*)(intptr_t)handle); +} + +JNIEXPORT jlong JNICALL Java_com_weaveffi_Vault_nativeOpen(JNIEnv* env, jclass clazz, jstring path) { + weaveffi_error err = {0, NULL, NULL, 0}; + const char* path_chars = (*env)->GetStringUTFChars(env, path, NULL); + void* rv = weaveffi_edge_cases_Vault_open(path_chars, &err); + (*env)->ReleaseStringUTFChars(env, path, path_chars); + if (err.code != 0) { + throw_weaveffi_edge_cases_EdgeErrors(env, &err); + return 0; + } + return (jlong)(intptr_t) rv; +} + +JNIEXPORT jboolean JNICALL Java_com_weaveffi_Vault_nativeProbe(JNIEnv* env, jclass clazz) { + weaveffi_error err = {0, NULL, NULL, 0}; + bool rv = weaveffi_edge_cases_Vault_probe(&err); + if (err.code != 0) { + throw_weaveffi_edge_cases_EdgeErrors(env, &err); + return JNI_FALSE; + } + return rv ? JNI_TRUE : JNI_FALSE; +} + +JNIEXPORT jobject JNICALL Java_com_weaveffi_Vault_nativePeek(JNIEnv* env, jclass clazz, jlong selfHandle) { + weaveffi_error err = {0, NULL, NULL, 0}; + weaveffi_edge_cases_Widget* rv = weaveffi_edge_cases_Vault_peek((const weaveffi_edge_cases_Vault*)(intptr_t)selfHandle, &err); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return NULL; + } + if (rv == NULL) { return NULL; } + jclass box_cls = (*env)->FindClass(env, "java/lang/Long"); + jmethodID box_mid = (*env)->GetStaticMethodID(env, box_cls, "valueOf", "(J)Ljava/lang/Long;"); + return (*env)->CallStaticObjectMethod(env, box_cls, box_mid, (jlong)(intptr_t)rv); +} + +JNIEXPORT void JNICALL Java_com_weaveffi_Vault_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) { + weaveffi_edge_cases_Vault_destroy((weaveffi_edge_cases_Vault*)(intptr_t)handle); +} + +JNIEXPORT jobjectArray JNICALL Java_com_weaveffi_EdgeCasesStringStreamIterator_nativeNext(JNIEnv* env, jclass clazz, jlong handle) { + weaveffi_edge_cases_StringStreamIterator* _iter = (weaveffi_edge_cases_StringStreamIterator*)(intptr_t)handle; + const char* _item = (const char*)0; + weaveffi_error err = {0, NULL, NULL, 0}; + int32_t _has = weaveffi_edge_cases_StringStreamIterator_next(_iter, &_item, &err); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return NULL; + } + if (_has == 0) { return NULL; } + jstring _jitem = _item ? (*env)->NewStringUTF(env, _item) : (*env)->NewStringUTF(env, ""); + weaveffi_free_string(_item); + jclass _obj_cls = (*env)->FindClass(env, "java/lang/Object"); + jobjectArray _slot = (*env)->NewObjectArray(env, 1, _obj_cls, NULL); + (*env)->SetObjectArrayElement(env, _slot, 0, _jitem); + return _slot; +} + +JNIEXPORT void JNICALL Java_com_weaveffi_EdgeCasesStringStreamIterator_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) { + weaveffi_edge_cases_StringStreamIterator_destroy((weaveffi_edge_cases_StringStreamIterator*)(intptr_t)handle); +} + +JNIEXPORT jobjectArray JNICALL Java_com_weaveffi_EdgeCasesScalarStreamIterator_nativeNext(JNIEnv* env, jclass clazz, jlong handle) { + weaveffi_edge_cases_ScalarStreamIterator* _iter = (weaveffi_edge_cases_ScalarStreamIterator*)(intptr_t)handle; + int32_t _item = (int32_t)0; + weaveffi_error err = {0, NULL, NULL, 0}; + int32_t _has = weaveffi_edge_cases_ScalarStreamIterator_next(_iter, &_item, &err); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return NULL; + } + if (_has == 0) { return NULL; } + jclass _jitem_cls = (*env)->FindClass(env, "java/lang/Integer"); + jobject _jitem = (*env)->CallStaticObjectMethod(env, _jitem_cls, (*env)->GetStaticMethodID(env, _jitem_cls, "valueOf", "(I)Ljava/lang/Integer;"), (jint)_item); + jclass _obj_cls = (*env)->FindClass(env, "java/lang/Object"); + jobjectArray _slot = (*env)->NewObjectArray(env, 1, _obj_cls, NULL); + (*env)->SetObjectArrayElement(env, _slot, 0, _jitem); + return _slot; +} + +JNIEXPORT void JNICALL Java_com_weaveffi_EdgeCasesScalarStreamIterator_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) { + weaveffi_edge_cases_ScalarStreamIterator_destroy((weaveffi_edge_cases_ScalarStreamIterator*)(intptr_t)handle); +} + +JNIEXPORT jobjectArray JNICALL Java_com_weaveffi_EdgeCasesModeStreamIterator_nativeNext(JNIEnv* env, jclass clazz, jlong handle) { + weaveffi_edge_cases_ModeStreamIterator* _iter = (weaveffi_edge_cases_ModeStreamIterator*)(intptr_t)handle; + weaveffi_edge_cases_Mode _item = (weaveffi_edge_cases_Mode)0; + weaveffi_error err = {0, NULL, NULL, 0}; + int32_t _has = weaveffi_edge_cases_ModeStreamIterator_next(_iter, &_item, &err); + if (err.code != 0) { + throw_weaveffi_error(env, &err); + return NULL; + } + if (_has == 0) { return NULL; } + jclass _jitem_cls = (*env)->FindClass(env, "java/lang/Integer"); + jobject _jitem = (*env)->CallStaticObjectMethod(env, _jitem_cls, (*env)->GetStaticMethodID(env, _jitem_cls, "valueOf", "(I)Ljava/lang/Integer;"), (jint)_item); + jclass _obj_cls = (*env)->FindClass(env, "java/lang/Object"); + jobjectArray _slot = (*env)->NewObjectArray(env, 1, _obj_cls, NULL); + (*env)->SetObjectArrayElement(env, _slot, 0, _jitem); + return _slot; +} + +JNIEXPORT void JNICALL Java_com_weaveffi_EdgeCasesModeStreamIterator_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) { + weaveffi_edge_cases_ModeStreamIterator_destroy((weaveffi_edge_cases_ModeStreamIterator*)(intptr_t)handle); +} + + +// END weaveffi_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/android_edge_cases__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap b/crates/weaveffi-cli/tests/snapshots/android_edge_cases__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap new file mode 100644 index 00000000..12e39169 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/android_edge_cases__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap @@ -0,0 +1,747 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from weaveffi.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate weaveffi.yml -o + +package com.weaveffi + +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +class WeaveFFI { + companion object { + init { System.loadLibrary("weaveffi") } + + @Volatile private var callbackExceptionHandler: ((Throwable) -> Unit)? = null + + /** + * Installs a handler for exceptions thrown by listener callbacks and + * async continuations on native producer threads. These exceptions have + * no Kotlin caller to propagate to; when no handler is installed, they + * are logged with their stack trace and dropped. Pass `null` to + * restore the default logging behavior. + */ + @JvmStatic fun setCallbackExceptionHandler(handler: ((Throwable) -> Unit)?) { + callbackExceptionHandler = handler + } + + // Invoked from the JNI glue; rethrowing signals "no handler" so the + // glue falls back to ExceptionDescribe. + @JvmStatic private fun dispatchCallbackException(t: Throwable) { + callbackExceptionHandler?.invoke(t) ?: throw t + } + + /** Exceptions thrown by the callback cannot propagate to a caller (events fire on a producer thread); they are delivered to the handler installed via [setCallbackExceptionHandler], or logged with their stack trace and dropped when no handler is set. */ + @JvmStatic private external fun registerBatchListenerJni(callback: (ByteArray, ByteArray, ByteArray, Int) -> Unit): Long + @JvmStatic fun registerBatchListener(callback: (List, ByteArray, Outer?, Int) -> Unit): Long = registerBatchListenerJni { a0, a1, a2, a3 -> callback(weaveDecode(a0) { r -> r.readList { unpackOuter(r) } }, a1, weaveDecode(a2) { r -> r.readOptional { unpackOuter(r) } }, a3) } + @JvmStatic external fun unregisterBatchListener(id: Long) + /** Exceptions thrown by the callback cannot propagate to a caller (events fire on a producer thread); they are delivered to the handler installed via [setCallbackExceptionHandler], or logged with their stack trace and dropped when no handler is set. */ + @JvmStatic external fun registerTickListener(callback: () -> Unit): Long + @JvmStatic external fun unregisterTickListener(id: Long) + /** + * No parameters and no result + */ + @JvmStatic external fun noop(): Unit + /** + * Function named after a keyword whose parameters are all keywords + */ + @JvmStatic private external fun selectJni(class_: Int, from: String, func: ByteArray, range: ByteArray, type: Boolean): Int + @JvmStatic fun select(class_: Int, from: String, func: List, range: Int?, type: Boolean): Int = selectJni(class_, from, weaveEncode { w -> w.writeList(func) { v0 -> w.writeString(v0) } }, weaveEncode { w -> w.writeOptional(range) { v0 -> w.writeI32(v0) } }, type) + /** + * Optional list of optional structs, the deepest legal nesting + */ + @JvmStatic private external fun nestedOptionalJni(items: ByteArray): ByteArray + @JvmStatic fun nestedOptional(items: List?): List? = weaveDecode(nestedOptionalJni(weaveEncode { w -> w.writeOptional(items) { v0 -> w.writeList(v0) { v1 -> w.writeOptional(v1) { v2 -> packOuter(w, v2) } } } })) { r -> r.readOptional { r.readList { r.readOptional { unpackOuter(r) } } } } + @JvmStatic private external fun optionalParamsJni(text: ByteArray, count: ByteArray, outer: ByteArray, tags: ByteArray, blob: ByteArray, mode: ByteArray): ByteArray + @JvmStatic fun optionalParams(text: String?, count: Int?, outer: Outer?, tags: List?, blob: ByteArray?, mode: Mode?): Mode? = weaveDecode(optionalParamsJni(weaveEncode { w -> w.writeOptional(text) { v0 -> w.writeString(v0) } }, weaveEncode { w -> w.writeOptional(count) { v0 -> w.writeI32(v0) } }, weaveEncode { w -> w.writeOptional(outer) { v0 -> packOuter(w, v0) } }, weaveEncode { w -> w.writeOptional(tags) { v0 -> w.writeList(v0) { v1 -> w.writeString(v1) } } }, weaveEncode { w -> w.writeOptional(blob) { v0 -> w.writeBytes(v0) } }, weaveEncode { w -> w.writeOptional(mode) { v0 -> w.writeI32(v0.value) } })) { r -> r.readOptional { Mode.fromValue(r.readI32()) } } + /** + * Write-back parameters + */ + @JvmStatic external fun mutableParams(text: String, blob: ByteArray): Unit + /** + * Factory for an interface with no constructor + */ + @JvmStatic private external fun makeWidgetJni(label: String): Long + @JvmStatic fun makeWidget(label: String): Widget = Widget(makeWidgetJni(label)) + /** + * Optional interface return + */ + @JvmStatic private external fun findWidgetJni(id: Long): Long? + @JvmStatic fun findWidget(id: Long): Widget? = findWidgetJni(id)?.let { Widget(it) } + /** + * Interface parameter, including an optional one + */ + @JvmStatic private external fun inspectWidgetJni(widget: Long, fallback: Long?): String + @JvmStatic fun inspectWidget(widget: Widget, fallback: Widget?): String = inspectWidgetJni(widget.handle, fallback?.handle) + @JvmStatic external fun typedHandleParam(token: Long, raw: Long): Long + /** + * Iterator of strings + */ + @JvmStatic private external fun stringStreamJni(): Long + @JvmStatic fun stringStream(): Iterator = EdgeCasesStringStreamIterator(stringStreamJni()) + /** + * Iterator of scalars + */ + @JvmStatic private external fun scalarStreamJni(): Long + @JvmStatic fun scalarStream(): Iterator = EdgeCasesScalarStreamIterator(scalarStreamJni()) + /** + * Iterator of enums + */ + @JvmStatic private external fun modeStreamJni(): Long + @JvmStatic fun modeStream(): Iterator = EdgeCasesModeStreamIterator(modeStreamJni()) + /** + * Async with no result + */ + @JvmStatic private external fun asyncVoidAsync(callback: Any) + @JvmStatic suspend fun asyncVoid(): Unit = suspendCancellableCoroutine { cont -> + asyncVoidAsync(WeaveContinuation(cont) { code, message, _ -> WeaveFFIException(code, message) }) + } + @JvmStatic private external fun asyncBytesAsync(size: Long, callback: Any) + @JvmStatic suspend fun asyncBytes(size: Long): ByteArray = suspendCancellableCoroutine { cont -> + asyncBytesAsync(size, WeaveContinuation(cont) { code, message, _ -> WeaveFFIException(code, message) }) + } + @JvmStatic private external fun asyncStructAsync(callback: Any) + @JvmStatic suspend fun asyncStruct(): Outer { + val raw: ByteArray = suspendCancellableCoroutine { cont -> + asyncStructAsync(WeaveContinuation(cont) { code, message, payload -> EdgeErrorsException.fromCode(code, message, payload) }) + } + return weaveDecode(raw) { r -> unpackOuter(r) } + } + @JvmStatic private external fun asyncListAsync(cancelToken: Long, callback: Any) + @JvmStatic suspend fun asyncList(): List { + val raw: ByteArray = suspendCancellableCoroutine { cont -> + asyncListAsync(0L, WeaveContinuation(cont) { code, message, payload -> EdgeErrorsException.fromCode(code, message, payload) }) + } + return weaveDecode(raw) { r -> r.readList { unpackOuter(r) } } + } + @JvmStatic private external fun asyncOptionalAsync(callback: Any) + @JvmStatic suspend fun asyncOptional(): Long? { + val raw: ByteArray = suspendCancellableCoroutine { cont -> + asyncOptionalAsync(WeaveContinuation(cont) { code, message, _ -> WeaveFFIException(code, message) }) + } + return weaveDecode(raw) { r -> r.readOptional { r.readI64() } } + } + @JvmStatic private external fun asyncEnumAsync(callback: Any) + @JvmStatic suspend fun asyncEnum(): Mode { + val raw: Int = suspendCancellableCoroutine { cont -> + asyncEnumAsync(WeaveContinuation(cont) { code, message, _ -> WeaveFFIException(code, message) }) + } + return Mode.fromValue(raw) + } + @JvmStatic private external fun asyncKeywordsAsync(class_: String, import: Int, callback: Any) + @JvmStatic suspend fun asyncKeywords(class_: String, import: Int): String = suspendCancellableCoroutine { cont -> + asyncKeywordsAsync(class_, import, WeaveContinuation(cont) { code, message, _ -> WeaveFFIException(code, message) }) + } + @JvmStatic private external fun keywordsRoundtripJni(value: ByteArray): ByteArray + @JvmStatic fun keywordsRoundtrip(value: Keywords): Keywords = weaveDecode(keywordsRoundtripJni(weaveEncode { w -> packKeywords(w, value) })) { r -> unpackKeywords(r) } + @JvmStatic private external fun legacyValueJni(value: Int): ByteArray + @Deprecated("Use Mode-based APIs") + @JvmStatic fun legacyValue(value: Legacy): Retired = weaveDecode(legacyValueJni(value.value)) { r -> unpackRetired(r) } + } +} + +/** Deprecated enum with non-contiguous values and keyword-like variants */ +enum class Legacy(val value: Int) { + None(0), + Default(5), + Class(10); + + companion object { + fun fromValue(value: Int): Legacy = entries.first { it.value == value } + } +} + +enum class Mode(val value: Int) { + Fast(1), + Safe(2); + + companion object { + fun fromValue(value: Int): Mode = entries.first { it.value == value } + } +} + +data class Leaf(val weight: Float) + +internal fun packLeaf(w: WeaveBufferWriter, v: Leaf) { + w.writeF32(v.weight) +} + +internal fun unpackLeaf(r: WeaveBufferReader): 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: WeaveBufferWriter, v: Inner) { + packLeaf(w, v.leaf) + w.writeList(v.leaves) { v0 -> packLeaf(w, v0) } +} + +internal fun unpackInner(r: WeaveBufferReader): Inner = Inner( + unpackLeaf(r), + r.readList { unpackLeaf(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: Byte) + +internal fun packOuter(w: WeaveBufferWriter, v: Outer) { + packInner(w, v.inner) + w.writeOptional(v.maybe_inner) { v0 -> packInner(w, v0) } + w.writeMap(v.by_name, { k0 -> w.writeString(k0) }, { v0 -> packInner(w, v0) }) + w.writeList(v.modes) { v0 -> w.writeI32(v0.value) } + w.writeList(v.matrix) { v0 -> w.writeList(v0) { v1 -> w.writeI32(v1) } } + w.writeList(v.sparse) { v0 -> w.writeOptional(v0) { v1 -> w.writeI64(v1) } } + w.writeOptional(v.blob) { v0 -> w.writeBytes(v0) } + w.writeOptional(v.note) { v0 -> w.writeString(v0) } + w.writeMap(v.by_id, { k0 -> w.writeI64(k0) }, { v0 -> w.writeString(v0) }) + w.writeBool(v._hidden) + w.writeI8(v.v2) +} + +internal fun unpackOuter(r: WeaveBufferReader): Outer = Outer( + unpackInner(r), + r.readOptional { unpackInner(r) }, + r.readMap({ r.readString() }, { unpackInner(r) }), + r.readList { Mode.fromValue(r.readI32()) }, + r.readList { r.readList { r.readI32() } }, + r.readList { r.readOptional { r.readI64() } }, + r.readOptional { r.readBytes() }, + r.readOptional { r.readString() }, + r.readMap({ r.readI64() }, { r.readString() }), + r.readBool(), + r.readI8(), +) + +/** 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: WeaveBufferWriter, 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: WeaveBufferReader): 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 */ +data class Retired(val id: Long) + +internal fun packRetired(w: WeaveBufferWriter, v: Retired) { + w.writeI64(v.id) +} + +internal fun unpackRetired(r: WeaveBufferReader): Retired = Retired( + r.readI64(), +) + +/** Interface with no constructor; instances come from make_widget */ +class Widget internal constructor(internal var handle: Long) : java.io.Closeable { + companion object { + init { System.loadLibrary("weaveffi") } + + @JvmStatic private external fun nativeRefresh(selfHandle: Long): Unit + @JvmStatic private external fun nativeRename(selfHandle: Long, class_: String, import: ByteArray): String + @JvmStatic private external fun nativeFetchAsync(selfHandle: Long, id: Long, callback: Any) + @JvmStatic private external fun nativeOldName(selfHandle: Long): String + @JvmStatic private external fun nativeDestroy(handle: Long) + } + + /** + * Void method that can fail + */ + fun refresh() { nativeRefresh(handle) } + + fun rename(class_: String, import: String?): String = nativeRename(handle, class_, weaveEncode { w -> w.writeOptional(import) { v0 -> w.writeString(v0) } }) + + /** + * Async method with a struct result + */ + suspend fun fetch(id: Long): Outer { + val raw: ByteArray = suspendCancellableCoroutine { cont -> + nativeFetchAsync(handle, id, WeaveContinuation(cont) { code, message, payload -> EdgeErrorsException.fromCode(code, message, payload) }) + } + return weaveDecode(raw) { r -> unpackOuter(r) } + } + + @Deprecated("Use rename instead") + fun oldName(): String = nativeOldName(handle) + + override fun close() { + if (handle != 0L) { + nativeDestroy(handle) + handle = 0L + } + } + + protected fun finalize() { + close() + } +} + +/** Deprecated interface with a throwing constructor */ +class Vault internal constructor(internal var handle: Long) : java.io.Closeable { + companion object { + init { System.loadLibrary("weaveffi") } + + @JvmStatic private external fun nativeOpen(path: String): Long + @JvmStatic private external fun nativeProbe(): Boolean + @JvmStatic private external fun nativePeek(selfHandle: Long): Long? + @JvmStatic private external fun nativeDestroy(handle: Long) + + /** + * Fails when the vault is busy + */ + fun open(path: String): Vault = Vault(nativeOpen(path)) + + /** + * Throwing static + */ + fun probe(): Boolean = nativeProbe() + } + + fun peek(): Widget? = nativePeek(handle)?.let { Widget(it) } + + override fun close() { + if (handle != 0L) { + nativeDestroy(handle) + handle = 0L + } + } + + protected fun finalize() { + close() + } +} + +/** + * A lazy iterator over the `String` elements streamed by [stringStream]. Each step pulls + * exactly one element from the native producer. The native handle is + * released when the producer is exhausted, when [close] is called, or by + * the finalizer if the iterator is abandoned, whichever comes first. + */ +class EdgeCasesStringStreamIterator internal constructor(private var handle: Long) : Iterator, java.io.Closeable { + private var nextSlot: Array? = null + + override fun hasNext(): Boolean { + if (nextSlot != null) return true + if (handle == 0L) return false + val slot = nativeNext(handle) + if (slot == null) { + close() + return false + } + nextSlot = slot + return true + } + + override fun next(): String { + if (!hasNext()) throw NoSuchElementException() + val raw = nextSlot!![0] + nextSlot = null + return raw as String + } + + override fun close() { + if (handle != 0L) { + nativeDestroy(handle) + handle = 0L + } + } + + protected fun finalize() { + close() + } + + companion object { + init { System.loadLibrary("weaveffi") } + + @JvmStatic private external fun nativeNext(handle: Long): Array? + @JvmStatic private external fun nativeDestroy(handle: Long) + } +} + +/** + * A lazy iterator over the `Int` elements streamed by [scalarStream]. Each step pulls + * exactly one element from the native producer. The native handle is + * released when the producer is exhausted, when [close] is called, or by + * the finalizer if the iterator is abandoned, whichever comes first. + */ +class EdgeCasesScalarStreamIterator internal constructor(private var handle: Long) : Iterator, java.io.Closeable { + private var nextSlot: Array? = null + + override fun hasNext(): Boolean { + if (nextSlot != null) return true + if (handle == 0L) return false + val slot = nativeNext(handle) + if (slot == null) { + close() + return false + } + nextSlot = slot + return true + } + + override fun next(): Int { + if (!hasNext()) throw NoSuchElementException() + val raw = nextSlot!![0] + nextSlot = null + return raw as Int + } + + override fun close() { + if (handle != 0L) { + nativeDestroy(handle) + handle = 0L + } + } + + protected fun finalize() { + close() + } + + companion object { + init { System.loadLibrary("weaveffi") } + + @JvmStatic private external fun nativeNext(handle: Long): Array? + @JvmStatic private external fun nativeDestroy(handle: Long) + } +} + +/** + * A lazy iterator over the `Mode` elements streamed by [modeStream]. Each step pulls + * exactly one element from the native producer. The native handle is + * released when the producer is exhausted, when [close] is called, or by + * the finalizer if the iterator is abandoned, whichever comes first. + */ +class EdgeCasesModeStreamIterator internal constructor(private var handle: Long) : Iterator, java.io.Closeable { + private var nextSlot: Array? = null + + override fun hasNext(): Boolean { + if (nextSlot != null) return true + if (handle == 0L) return false + val slot = nativeNext(handle) + if (slot == null) { + close() + return false + } + nextSlot = slot + return true + } + + override fun next(): Mode { + if (!hasNext()) throw NoSuchElementException() + val raw = nextSlot!![0] + nextSlot = null + return Mode.fromValue(raw as Int) + } + + override fun close() { + if (handle != 0L) { + nativeDestroy(handle) + handle = 0L + } + } + + protected fun finalize() { + close() + } + + companion object { + init { System.loadLibrary("weaveffi") } + + @JvmStatic private external fun nativeNext(handle: Long): Array? + @JvmStatic private external fun nativeDestroy(handle: Long) + } +} + +/** Generic WeaveFFI failure: panics, marshalling errors, and unknown codes. */ +open class WeaveFFIException(val code: Int, message: String) : Exception(message) + +/** Typed error domain `EdgeErrors` declared by module `edge_cases`. */ +sealed class EdgeErrorsException(code: Int, message: String) : WeaveFFIException(code, message) { + class Busy(message: String = "Resource is busy") : EdgeErrorsException(7, message) + + companion object { + /** Map a raw `EdgeErrors` code and payload to the typed exception; unknown codes yield the generic [WeaveFFIException]. */ + @JvmStatic fun fromCode(code: Int, message: String, payload: ByteArray?): WeaveFFIException = when (code) { + 7 -> Busy(message) + else -> WeaveFFIException(code, message) + } + } +} + +/** Growable little-endian writer implementing the WeaveFFI value-buffer wire format. */ +internal class WeaveBufferWriter { + private var buf = ByteArray(32) + private var len = 0 + + 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 writeI16(v: Short) { + reserve(2) + val b = v.toInt() + buf[len++] = (b and 0xFF).toByte() + buf[len++] = ((b shr 8) and 0xFF).toByte() + } + fun writeI32(v: Int) { + reserve(4) + buf[len++] = (v and 0xFF).toByte() + buf[len++] = ((v shr 8) and 0xFF).toByte() + buf[len++] = ((v shr 16) and 0xFF).toByte() + buf[len++] = ((v shr 24) and 0xFF).toByte() + } + fun writeU32(v: Long) = writeI32(v.toInt()) + fun writeI64(v: Long) { + writeI32(v.toInt()) + writeI32((v ushr 32).toInt()) + } + fun writeF32(v: Float) = writeI32(v.toRawBits()) + fun writeF64(v: Double) = writeI64(v.toRawBits()) + fun writeString(v: String) = writeBytes(v.toByteArray(Charsets.UTF_8)) + fun writeBytes(v: ByteArray) { + writeI32(v.size) + reserve(v.size) + v.copyInto(buf, len) + len += v.size + } + fun writeOptional(v: T?, write: (T) -> Unit) { + if (v == null) writeBool(false) else { writeBool(true); write(v) } + } + fun writeList(v: List, write: (T) -> Unit) { + writeI32(v.size) + for (e in v) write(e) + } + 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 WeaveFFI value-buffer wire format. */ +internal class WeaveBufferReader(private val buf: ByteArray) { + private var pos = 0 + + private fun malformed(detail: String): Nothing = + throw WeaveFFIException(-2, "malformed WeaveFFI 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 readI16(): Short { + val at = take(2) + return ((buf[at].toInt() and 0xFF) or ((buf[at + 1].toInt() and 0xFF) shl 8)).toShort() + } + 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(): Long = readI32().toLong() and 0xFFFFFFFFL + fun readI64(): Long { + val lo = readI32().toLong() and 0xFFFFFFFFL + val hi = readI32().toLong() + return lo or (hi shl 32) + } + 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) + } + fun readOptional(read: () -> T): T? = if (readBool()) read() else null + fun readList(read: () -> T): List { + val n = readLen() + val out = ArrayList(n) + repeat(n) { out.add(read()) } + return out + } + fun readMap(readKey: () -> K, readValue: () -> V): Map { + val n = readLen() + val out = LinkedHashMap() + repeat(n) { out[readKey()] = readValue() } + return out + } + fun expectEnd() { + if (pos != buf.size) malformed("trailing bytes after value") + } +} + +internal fun weaveEncode(write: (WeaveBufferWriter) -> Unit): ByteArray { + val w = WeaveBufferWriter() + write(w) + return w.toByteArray() +} + +internal fun weaveDecode(bytes: ByteArray, read: (WeaveBufferReader) -> T): T { + val r = WeaveBufferReader(bytes) + val v = read(r) + r.expectEnd() + return v +} + +internal class WeaveContinuation( + private val cont: kotlinx.coroutines.CancellableContinuation, + private val mapError: (Int, String, ByteArray?) -> Throwable +) { + @Suppress("UNCHECKED_CAST") + fun onSuccess(result: Any?) { cont.resume(result as T) } + fun onError(code: Int, message: String, payload: ByteArray?) { cont.resumeWithException(mapError(code, message, payload)) } +} + +// END WeaveFFI.kt diff --git a/crates/weaveffi-cli/tests/snapshots/android_events__build_gradle.snap b/crates/weaveffi-cli/tests/snapshots/android_events__build_gradle.snap deleted file mode 100644 index 5cf5c482..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_events__build_gradle.snap +++ /dev/null @@ -1,31 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -plugins { - id 'com.android.library' - id 'org.jetbrains.kotlin.android' version '1.9.22' apply false -} - -android { - namespace 'com.weaveffi' - compileSdk 34 - defaultConfig { - minSdk 24 - externalNativeBuild { - cmake { - cppFlags "" - } - } - } - externalNativeBuild { - cmake { - path "src/main/cpp/CMakeLists.txt" - } - } -} - -// END build.gradle diff --git a/crates/weaveffi-cli/tests/snapshots/android_events__settings_gradle.snap b/crates/weaveffi-cli/tests/snapshots/android_events__settings_gradle.snap deleted file mode 100644 index f10a8b92..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_events__settings_gradle.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -rootProject.name = 'weaveffi' - -// END settings.gradle diff --git a/crates/weaveffi-cli/tests/snapshots/android_events__src_main_cpp_CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/android_events__src_main_cpp_CMakeLists_txt.snap deleted file mode 100644 index e2c5c99f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_events__src_main_cpp_CMakeLists_txt.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -cmake_minimum_required(VERSION 3.22) -project(weaveffi) -add_library(weaveffi SHARED weaveffi_jni.c) -target_include_directories(weaveffi PRIVATE ../../../../c) - -# END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/android_events__src_main_cpp_weaveffi_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/android_events__src_main_cpp_weaveffi_jni_c.snap deleted file mode 100644 index 40976f19..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_events__src_main_cpp_weaveffi_jni_c.snap +++ /dev/null @@ -1,164 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include -#include -#include -#include -#include -#include -#include "weaveffi.h" - -static void throw_weaveffi_error(JNIEnv* env, weaveffi_error* err) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jclass exClass = (*env)->FindClass(env, "com/weaveffi/WeaveFFIException"); - if (exClass != NULL) { - jmethodID ctor = (*env)->GetMethodID(env, exClass, "", "(ILjava/lang/String;)V"); - jstring jmsg = (*env)->NewStringUTF(env, msg); - jthrowable ex = (jthrowable)(*env)->NewObject(env, exClass, ctor, (jint)err->code, jmsg); - if (ex != NULL) { (*env)->Throw(env, ex); } - } - weaveffi_error_clear(err); -} - -static jclass weaveffi_jni_entry_class = NULL; -static jmethodID weaveffi_jni_dispatch_exc = NULL; - -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { - (void)reserved; - JNIEnv* env = NULL; - if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } - jclass cls = (*env)->FindClass(env, "com/weaveffi/WeaveFFI"); - if (cls == NULL) { (*env)->ExceptionClear(env); return JNI_VERSION_1_6; } - weaveffi_jni_entry_class = (jclass)(*env)->NewGlobalRef(env, cls); - weaveffi_jni_dispatch_exc = (*env)->GetStaticMethodID(env, weaveffi_jni_entry_class, "dispatchCallbackException", "(Ljava/lang/Throwable;)V"); - if (weaveffi_jni_dispatch_exc == NULL) { (*env)->ExceptionClear(env); } - return JNI_VERSION_1_6; -} - -static void weaveffi_jni_handle_uncaught(JNIEnv* env) { - if (!(*env)->ExceptionCheck(env)) { return; } - jthrowable ex = (*env)->ExceptionOccurred(env); - (*env)->ExceptionClear(env); - if (weaveffi_jni_entry_class != NULL && weaveffi_jni_dispatch_exc != NULL) { - (*env)->CallStaticVoidMethod(env, weaveffi_jni_entry_class, weaveffi_jni_dispatch_exc, ex); - if (!(*env)->ExceptionCheck(env)) { (*env)->DeleteLocalRef(env, ex); return; } - (*env)->ExceptionClear(env); - } - (*env)->Throw(env, ex); - (*env)->ExceptionDescribe(env); - (*env)->ExceptionClear(env); - (*env)->DeleteLocalRef(env, ex); -} - -typedef struct weaveffi_jni_listener_ctx { - JavaVM* jvm; - jobject callback; - uint64_t id; - struct weaveffi_jni_listener_ctx* next; -} weaveffi_jni_listener_ctx; - -static weaveffi_jni_listener_ctx* weaveffi_jni_listeners = NULL; -static pthread_mutex_t weaveffi_jni_listener_lock = PTHREAD_MUTEX_INITIALIZER; - -static void weaveffi_events_OnMessage_fn_jni_tramp(const char* message, void* context) { - weaveffi_jni_listener_ctx* ctx = (weaveffi_jni_listener_ctx*)context; - JNIEnv* env = NULL; - int attached = 0; - if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { - if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) return; - attached = 1; - } - if ((*env)->PushLocalFrame(env, 32) != 0) { - if (attached) (*ctx->jvm)->DetachCurrentThread(ctx->jvm); - return; - } - jobject _a0 = message ? (jobject)(*env)->NewStringUTF(env, message) : (jobject)(*env)->NewStringUTF(env, ""); - jclass fn_cls = (*env)->GetObjectClass(env, ctx->callback); - jmethodID invoke = (*env)->GetMethodID(env, fn_cls, "invoke", "(Ljava/lang/Object;)Ljava/lang/Object;"); - (*env)->CallObjectMethod(env, ctx->callback, invoke, _a0); - weaveffi_jni_handle_uncaught(env); - (*env)->PopLocalFrame(env, NULL); - if (attached) (*ctx->jvm)->DetachCurrentThread(ctx->jvm); -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_registerMessageListener(JNIEnv* env, jclass clazz, jobject callback) { - weaveffi_jni_listener_ctx* ctx = (weaveffi_jni_listener_ctx*)calloc(1, sizeof(weaveffi_jni_listener_ctx)); - (*env)->GetJavaVM(env, &ctx->jvm); - ctx->callback = (*env)->NewGlobalRef(env, callback); - uint64_t id = weaveffi_events_register_message_listener(weaveffi_events_OnMessage_fn_jni_tramp, ctx); - ctx->id = id; - pthread_mutex_lock(&weaveffi_jni_listener_lock); - ctx->next = weaveffi_jni_listeners; - weaveffi_jni_listeners = ctx; - pthread_mutex_unlock(&weaveffi_jni_listener_lock); - return (jlong)id; -} - -JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_unregisterMessageListener(JNIEnv* env, jclass clazz, jlong id) { - weaveffi_events_unregister_message_listener((uint64_t)id); - pthread_mutex_lock(&weaveffi_jni_listener_lock); - weaveffi_jni_listener_ctx** link = &weaveffi_jni_listeners; - while (*link != NULL) { - if ((*link)->id == (uint64_t)id) { - weaveffi_jni_listener_ctx* found = *link; - *link = found->next; - (*env)->DeleteGlobalRef(env, found->callback); - free(found); - break; - } - link = &(*link)->next; - } - pthread_mutex_unlock(&weaveffi_jni_listener_lock); -} - -JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_sendMessage(JNIEnv* env, jclass clazz, jstring text) { - weaveffi_error err = {0, NULL, NULL, 0}; - const char* text_chars = (*env)->GetStringUTFChars(env, text, NULL); - weaveffi_events_send_message(text_chars, &err); - (*env)->ReleaseStringUTFChars(env, text, text_chars); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - - } - return; -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_getMessagesJni(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - weaveffi_events_GetMessagesIterator* _iter = weaveffi_events_get_messages(&err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jlong)(intptr_t)_iter; -} - -JNIEXPORT jobjectArray JNICALL Java_com_weaveffi_EventsGetMessagesIterator_nativeNext(JNIEnv* env, jclass clazz, jlong handle) { - weaveffi_events_GetMessagesIterator* _iter = (weaveffi_events_GetMessagesIterator*)(intptr_t)handle; - const char* _item = (const char*)0; - weaveffi_error err = {0, NULL, NULL, 0}; - int32_t _has = weaveffi_events_GetMessagesIterator_next(_iter, &_item, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return NULL; - } - if (_has == 0) { return NULL; } - jstring _jitem = _item ? (*env)->NewStringUTF(env, _item) : (*env)->NewStringUTF(env, ""); - weaveffi_free_string(_item); - jclass _obj_cls = (*env)->FindClass(env, "java/lang/Object"); - jobjectArray _slot = (*env)->NewObjectArray(env, 1, _obj_cls, NULL); - (*env)->SetObjectArrayElement(env, _slot, 0, _jitem); - return _slot; -} - -JNIEXPORT void JNICALL Java_com_weaveffi_EventsGetMessagesIterator_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) { - weaveffi_events_GetMessagesIterator_destroy((weaveffi_events_GetMessagesIterator*)(intptr_t)handle); -} - - -// END weaveffi_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/android_events__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap b/crates/weaveffi-cli/tests/snapshots/android_events__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap deleted file mode 100644 index fc55028a..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_events__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap +++ /dev/null @@ -1,98 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -package com.weaveffi - -class WeaveFFI { - companion object { - init { System.loadLibrary("weaveffi") } - - @Volatile private var callbackExceptionHandler: ((Throwable) -> Unit)? = null - - /** - * Installs a handler for exceptions thrown by listener callbacks and - * async continuations on native producer threads. These exceptions have - * no Kotlin caller to propagate to; when no handler is installed, they - * are logged with their stack trace and dropped. Pass `null` to - * restore the default logging behavior. - */ - @JvmStatic fun setCallbackExceptionHandler(handler: ((Throwable) -> Unit)?) { - callbackExceptionHandler = handler - } - - // Invoked from the JNI glue; rethrowing signals "no handler" so the - // glue falls back to ExceptionDescribe. - @JvmStatic private fun dispatchCallbackException(t: Throwable) { - callbackExceptionHandler?.invoke(t) ?: throw t - } - - /** Exceptions thrown by the callback cannot propagate to a caller (events fire on a producer thread); they are delivered to the handler installed via [setCallbackExceptionHandler], or logged with their stack trace and dropped when no handler is set. */ - @JvmStatic external fun registerMessageListener(callback: (String) -> Unit): Long - @JvmStatic external fun unregisterMessageListener(id: Long) - /** - * Send a message, triggering the OnMessage callback - */ - @JvmStatic external fun sendMessage(text: String): Unit - /** - * Return an iterator over all sent messages - */ - @JvmStatic private external fun getMessagesJni(): Long - @JvmStatic fun getMessages(): Iterator = EventsGetMessagesIterator(getMessagesJni()) - } -} - -/** - * A lazy iterator over the `String` elements streamed by [getMessages]. Each step pulls - * exactly one element from the native producer. The native handle is - * released when the producer is exhausted, when [close] is called, or by - * the finalizer if the iterator is abandoned, whichever comes first. - */ -class EventsGetMessagesIterator internal constructor(private var handle: Long) : Iterator, java.io.Closeable { - private var nextSlot: Array? = null - - override fun hasNext(): Boolean { - if (nextSlot != null) return true - if (handle == 0L) return false - val slot = nativeNext(handle) - if (slot == null) { - close() - return false - } - nextSlot = slot - return true - } - - override fun next(): String { - if (!hasNext()) throw NoSuchElementException() - val raw = nextSlot!![0] - nextSlot = null - return raw as String - } - - override fun close() { - if (handle != 0L) { - nativeDestroy(handle) - handle = 0L - } - } - - protected fun finalize() { - close() - } - - companion object { - init { System.loadLibrary("weaveffi") } - - @JvmStatic private external fun nativeNext(handle: Long): Array? - @JvmStatic private external fun nativeDestroy(handle: Long) - } -} - -/** Generic WeaveFFI failure: panics, marshalling errors, and unknown codes. */ -open class WeaveFFIException(val code: Int, message: String) : Exception(message) - -// END WeaveFFI.kt diff --git a/crates/weaveffi-cli/tests/snapshots/android_inventory__build_gradle.snap b/crates/weaveffi-cli/tests/snapshots/android_inventory__build_gradle.snap deleted file mode 100644 index 5cf5c482..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_inventory__build_gradle.snap +++ /dev/null @@ -1,31 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -plugins { - id 'com.android.library' - id 'org.jetbrains.kotlin.android' version '1.9.22' apply false -} - -android { - namespace 'com.weaveffi' - compileSdk 34 - defaultConfig { - minSdk 24 - externalNativeBuild { - cmake { - cppFlags "" - } - } - } - externalNativeBuild { - cmake { - path "src/main/cpp/CMakeLists.txt" - } - } -} - -// END build.gradle diff --git a/crates/weaveffi-cli/tests/snapshots/android_inventory__settings_gradle.snap b/crates/weaveffi-cli/tests/snapshots/android_inventory__settings_gradle.snap deleted file mode 100644 index f10a8b92..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_inventory__settings_gradle.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -rootProject.name = 'weaveffi' - -// END settings.gradle diff --git a/crates/weaveffi-cli/tests/snapshots/android_inventory__src_main_cpp_CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/android_inventory__src_main_cpp_CMakeLists_txt.snap deleted file mode 100644 index e2c5c99f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_inventory__src_main_cpp_CMakeLists_txt.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -cmake_minimum_required(VERSION 3.22) -project(weaveffi) -add_library(weaveffi SHARED weaveffi_jni.c) -target_include_directories(weaveffi PRIVATE ../../../../c) - -# END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/android_inventory__src_main_cpp_weaveffi_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/android_inventory__src_main_cpp_weaveffi_jni_c.snap deleted file mode 100644 index b7a08d53..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_inventory__src_main_cpp_weaveffi_jni_c.snap +++ /dev/null @@ -1,196 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include -#include -#include -#include -#include -#include "weaveffi.h" - -static void throw_weaveffi_error(JNIEnv* env, weaveffi_error* err) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jclass exClass = (*env)->FindClass(env, "com/weaveffi/WeaveFFIException"); - if (exClass != NULL) { - jmethodID ctor = (*env)->GetMethodID(env, exClass, "", "(ILjava/lang/String;)V"); - jstring jmsg = (*env)->NewStringUTF(env, msg); - jthrowable ex = (jthrowable)(*env)->NewObject(env, exClass, ctor, (jint)err->code, jmsg); - if (ex != NULL) { (*env)->Throw(env, ex); } - } - weaveffi_error_clear(err); -} - -static void throw_weaveffi_products_ProductsError(JNIEnv* env, weaveffi_error* err) { - jclass exClass = (*env)->FindClass(env, "com/weaveffi/ProductsException"); - jmethodID fromCode = exClass ? (*env)->GetStaticMethodID(env, exClass, "fromCode", "(ILjava/lang/String;[B)Lcom/weaveffi/WeaveFFIException;") : NULL; - if (fromCode == NULL) { - (*env)->ExceptionClear(env); - throw_weaveffi_error(env, err); - return; - } - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = (*env)->NewStringUTF(env, msg); - jbyteArray jpayload = NULL; - if (err->payload_ptr != NULL) { - jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); - if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } - } - jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, exClass, fromCode, (jint)err->code, jmsg, jpayload); - if (ex != NULL && !(*env)->ExceptionCheck(env)) { (*env)->Throw(env, ex); } - weaveffi_error_clear(err); -} - -static void throw_weaveffi_orders_OrdersError(JNIEnv* env, weaveffi_error* err) { - jclass exClass = (*env)->FindClass(env, "com/weaveffi/OrdersException"); - jmethodID fromCode = exClass ? (*env)->GetStaticMethodID(env, exClass, "fromCode", "(ILjava/lang/String;[B)Lcom/weaveffi/WeaveFFIException;") : NULL; - if (fromCode == NULL) { - (*env)->ExceptionClear(env); - throw_weaveffi_error(env, err); - return; - } - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = (*env)->NewStringUTF(env, msg); - jbyteArray jpayload = NULL; - if (err->payload_ptr != NULL) { - jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); - if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } - } - jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, exClass, fromCode, (jint)err->code, jmsg, jpayload); - if (ex != NULL && !(*env)->ExceptionCheck(env)) { (*env)->Throw(env, ex); } - weaveffi_error_clear(err); -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_createOrderJni(JNIEnv* env, jclass clazz, jbyteArray items) { - weaveffi_error err = {0, NULL, NULL, 0}; - jbyte* items_elems = (*env)->GetByteArrayElements(env, items, NULL); - jsize items_len = (*env)->GetArrayLength(env, items); - int64_t rv = weaveffi_orders_create_order((const uint8_t*)items_elems, (size_t)items_len, &err); - (*env)->ReleaseByteArrayElements(env, items, items_elems, JNI_ABORT); - if (err.code != 0) { - throw_weaveffi_orders_OrdersError(env, &err); - return 0; - } - return (jlong) rv; -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_getOrderJni(JNIEnv* env, jclass clazz, jlong id) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_orders_get_order((int64_t)id, &out_len, &err); - if (err.code != 0) { - throw_weaveffi_orders_OrdersError(env, &err); - return NULL; - } - jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len); - if (out && rv) { (*env)->SetByteArrayRegion(env, out, 0, (jsize)out_len, (const jbyte*)rv); } - weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len); - return out; -} - -JNIEXPORT jboolean JNICALL Java_com_weaveffi_WeaveFFI_cancelOrder(JNIEnv* env, jclass clazz, jlong id) { - weaveffi_error err = {0, NULL, NULL, 0}; - bool rv = weaveffi_orders_cancel_order((int64_t)id, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return JNI_FALSE; - } - return rv ? JNI_TRUE : JNI_FALSE; -} - -JNIEXPORT jboolean JNICALL Java_com_weaveffi_WeaveFFI_addProductToOrderJni(JNIEnv* env, jclass clazz, jlong order_id, jbyteArray product) { - weaveffi_error err = {0, NULL, NULL, 0}; - jbyte* product_elems = (*env)->GetByteArrayElements(env, product, NULL); - jsize product_len = (*env)->GetArrayLength(env, product); - bool rv = weaveffi_orders_add_product_to_order((int64_t)order_id, (const uint8_t*)product_elems, (size_t)product_len, &err); - (*env)->ReleaseByteArrayElements(env, product, product_elems, JNI_ABORT); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return JNI_FALSE; - } - return rv ? JNI_TRUE : JNI_FALSE; -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_Catalog_nativeNew(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - void* rv = weaveffi_products_Catalog_new(&err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jlong)(intptr_t) rv; -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_Catalog_nativeAddProduct(JNIEnv* env, jclass clazz, jlong selfHandle, jstring name, jdouble price, jint category) { - weaveffi_error err = {0, NULL, NULL, 0}; - const char* name_chars = (*env)->GetStringUTFChars(env, name, NULL); - size_t out_len = 0; - const uint8_t* rv = weaveffi_products_Catalog_add_product((const weaveffi_products_Catalog*)(intptr_t)selfHandle, name_chars, (double)price, (int32_t)category, &out_len, &err); - (*env)->ReleaseStringUTFChars(env, name, name_chars); - if (err.code != 0) { - throw_weaveffi_products_ProductsError(env, &err); - return NULL; - } - jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len); - if (out && rv) { (*env)->SetByteArrayRegion(env, out, 0, (jsize)out_len, (const jbyte*)rv); } - weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len); - return out; -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_Catalog_nativeGetProduct(JNIEnv* env, jclass clazz, jlong selfHandle, jlong id) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_products_Catalog_get_product((const weaveffi_products_Catalog*)(intptr_t)selfHandle, (int64_t)id, &out_len, &err); - if (err.code != 0) { - throw_weaveffi_products_ProductsError(env, &err); - return NULL; - } - jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len); - if (out && rv) { (*env)->SetByteArrayRegion(env, out, 0, (jsize)out_len, (const jbyte*)rv); } - weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len); - return out; -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_Catalog_nativeSearch(JNIEnv* env, jclass clazz, jlong selfHandle, jint category) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_products_Catalog_search((const weaveffi_products_Catalog*)(intptr_t)selfHandle, (int32_t)category, &out_len, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return NULL; - } - jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len); - if (out && rv) { (*env)->SetByteArrayRegion(env, out, 0, (jsize)out_len, (const jbyte*)rv); } - weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len); - return out; -} - -JNIEXPORT jboolean JNICALL Java_com_weaveffi_Catalog_nativeUpdatePrice(JNIEnv* env, jclass clazz, jlong selfHandle, jlong id, jdouble price) { - weaveffi_error err = {0, NULL, NULL, 0}; - bool rv = weaveffi_products_Catalog_update_price((const weaveffi_products_Catalog*)(intptr_t)selfHandle, (int64_t)id, (double)price, &err); - if (err.code != 0) { - throw_weaveffi_products_ProductsError(env, &err); - return JNI_FALSE; - } - return rv ? JNI_TRUE : JNI_FALSE; -} - -JNIEXPORT jboolean JNICALL Java_com_weaveffi_Catalog_nativeRemove(JNIEnv* env, jclass clazz, jlong selfHandle, jlong id) { - weaveffi_error err = {0, NULL, NULL, 0}; - bool rv = weaveffi_products_Catalog_remove((const weaveffi_products_Catalog*)(intptr_t)selfHandle, (int64_t)id, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return JNI_FALSE; - } - return rv ? JNI_TRUE : JNI_FALSE; -} - -JNIEXPORT void JNICALL Java_com_weaveffi_Catalog_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) { - weaveffi_products_Catalog_destroy((weaveffi_products_Catalog*)(intptr_t)handle); -} - - -// END weaveffi_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/android_inventory__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap b/crates/weaveffi-cli/tests/snapshots/android_inventory__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap deleted file mode 100644 index 80271100..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_inventory__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap +++ /dev/null @@ -1,301 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -package com.weaveffi - -class WeaveFFI { - companion object { - init { System.loadLibrary("weaveffi") } - - @JvmStatic private external fun createOrderJni(items: ByteArray): Long - @JvmStatic fun createOrder(items: List): Long = createOrderJni(weaveEncode { w -> w.writeList(items) { v0 -> packOrderItem(w, v0) } }) - @JvmStatic private external fun getOrderJni(id: Long): ByteArray - @JvmStatic fun getOrder(id: Long): Order = weaveDecode(getOrderJni(id)) { r -> unpackOrder(r) } - @JvmStatic external fun cancelOrder(id: Long): Boolean - @JvmStatic private external fun addProductToOrderJni(orderId: Long, product: ByteArray): Boolean - @JvmStatic fun addProductToOrder(orderId: Long, product: Product): Boolean = addProductToOrderJni(orderId, weaveEncode { w -> packProduct(w, product) }) - } -} - -enum class Category(val value: Int) { - Electronics(0), - Clothing(1), - Food(2), - Books(3); - - companion object { - fun fromValue(value: Int): Category = entries.first { it.value == value } - } -} - -data class Product(val id: Long, val name: String, val description: String?, val price: Double, val category: Category, val tags: List) - -internal fun packProduct(w: WeaveBufferWriter, v: Product) { - w.writeI64(v.id) - w.writeString(v.name) - w.writeOptional(v.description) { v0 -> w.writeString(v0) } - w.writeF64(v.price) - w.writeI32(v.category.value) - w.writeList(v.tags) { v0 -> w.writeString(v0) } -} - -internal fun unpackProduct(r: WeaveBufferReader): Product = Product( - r.readI64(), - r.readString(), - r.readOptional { r.readString() }, - r.readF64(), - Category.fromValue(r.readI32()), - r.readList { r.readString() }, -) - -/** A product catalog owning its products */ -class Catalog internal constructor(internal var handle: Long) : java.io.Closeable { - companion object { - init { System.loadLibrary("weaveffi") } - - @JvmStatic private external fun nativeNew(): Long - @JvmStatic private external fun nativeAddProduct(selfHandle: Long, name: String, price: Double, category: Int): ByteArray - @JvmStatic private external fun nativeGetProduct(selfHandle: Long, id: Long): ByteArray - @JvmStatic private external fun nativeSearch(selfHandle: Long, category: Int): ByteArray - @JvmStatic private external fun nativeUpdatePrice(selfHandle: Long, id: Long, price: Double): Boolean - @JvmStatic private external fun nativeRemove(selfHandle: Long, id: Long): Boolean - @JvmStatic private external fun nativeDestroy(handle: Long) - - operator fun invoke(): Catalog = Catalog(nativeNew()) - } - - fun addProduct(name: String, price: Double, category: Category): Product = weaveDecode(nativeAddProduct(handle, name, price, category.value)) { r -> unpackProduct(r) } - - fun getProduct(id: Long): Product = weaveDecode(nativeGetProduct(handle, id)) { r -> unpackProduct(r) } - - fun search(category: Category): List = weaveDecode(nativeSearch(handle, category.value)) { r -> r.readList { unpackProduct(r) } } - - fun updatePrice(id: Long, price: Double): Boolean = nativeUpdatePrice(handle, id, price) - - fun remove(id: Long): Boolean = nativeRemove(handle, id) - - override fun close() { - if (handle != 0L) { - nativeDestroy(handle) - handle = 0L - } - } - - protected fun finalize() { - close() - } -} - -data class OrderItem(val product_id: Long, val quantity: Int, val unit_price: Double) - -internal fun packOrderItem(w: WeaveBufferWriter, v: OrderItem) { - w.writeI64(v.product_id) - w.writeI32(v.quantity) - w.writeF64(v.unit_price) -} - -internal fun unpackOrderItem(r: WeaveBufferReader): OrderItem = OrderItem( - r.readI64(), - r.readI32(), - r.readF64(), -) - -data class Order(val id: Long, val items: List, val total: Double, val status: String) - -internal fun packOrder(w: WeaveBufferWriter, v: Order) { - w.writeI64(v.id) - w.writeList(v.items) { v0 -> packOrderItem(w, v0) } - w.writeF64(v.total) - w.writeString(v.status) -} - -internal fun unpackOrder(r: WeaveBufferReader): Order = Order( - r.readI64(), - r.readList { unpackOrderItem(r) }, - r.readF64(), - r.readString(), -) - -/** Generic WeaveFFI failure: panics, marshalling errors, and unknown codes. */ -open class WeaveFFIException(val code: Int, message: String) : Exception(message) - -/** Typed error domain `ProductsError` declared by module `products`. */ -sealed class ProductsException(code: Int, message: String) : WeaveFFIException(code, message) { - class InvalidPrice(message: String = "price must be positive") : ProductsException(1, message) - class ProductNotFound(message: String = "product not found") : ProductsException(2, message) - - companion object { - /** Map a raw `ProductsError` code and payload to the typed exception; unknown codes yield the generic [WeaveFFIException]. */ - @JvmStatic fun fromCode(code: Int, message: String, payload: ByteArray?): WeaveFFIException = when (code) { - 1 -> InvalidPrice(message) - 2 -> ProductNotFound(message) - else -> WeaveFFIException(code, message) - } - } -} - -/** Typed error domain `OrdersError` declared by module `orders`. */ -sealed class OrdersException(code: Int, message: String) : WeaveFFIException(code, message) { - class OrderNotFound(message: String = "order not found") : OrdersException(1, message) - class EmptyOrder(message: String = "order must contain at least one item") : OrdersException(2, message) - - companion object { - /** Map a raw `OrdersError` code and payload to the typed exception; unknown codes yield the generic [WeaveFFIException]. */ - @JvmStatic fun fromCode(code: Int, message: String, payload: ByteArray?): WeaveFFIException = when (code) { - 1 -> OrderNotFound(message) - 2 -> EmptyOrder(message) - else -> WeaveFFIException(code, message) - } - } -} - -/** Growable little-endian writer implementing the WeaveFFI value-buffer wire format. */ -internal class WeaveBufferWriter { - private var buf = ByteArray(32) - private var len = 0 - - 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 writeI16(v: Short) { - reserve(2) - val b = v.toInt() - buf[len++] = (b and 0xFF).toByte() - buf[len++] = ((b shr 8) and 0xFF).toByte() - } - fun writeI32(v: Int) { - reserve(4) - buf[len++] = (v and 0xFF).toByte() - buf[len++] = ((v shr 8) and 0xFF).toByte() - buf[len++] = ((v shr 16) and 0xFF).toByte() - buf[len++] = ((v shr 24) and 0xFF).toByte() - } - fun writeU32(v: Long) = writeI32(v.toInt()) - fun writeI64(v: Long) { - writeI32(v.toInt()) - writeI32((v ushr 32).toInt()) - } - fun writeF32(v: Float) = writeI32(v.toRawBits()) - fun writeF64(v: Double) = writeI64(v.toRawBits()) - fun writeString(v: String) = writeBytes(v.toByteArray(Charsets.UTF_8)) - fun writeBytes(v: ByteArray) { - writeI32(v.size) - reserve(v.size) - v.copyInto(buf, len) - len += v.size - } - fun writeOptional(v: T?, write: (T) -> Unit) { - if (v == null) writeBool(false) else { writeBool(true); write(v) } - } - fun writeList(v: List, write: (T) -> Unit) { - writeI32(v.size) - for (e in v) write(e) - } - 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 WeaveFFI value-buffer wire format. */ -internal class WeaveBufferReader(private val buf: ByteArray) { - private var pos = 0 - - private fun malformed(detail: String): Nothing = - throw WeaveFFIException(-2, "malformed WeaveFFI 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 readI16(): Short { - val at = take(2) - return ((buf[at].toInt() and 0xFF) or ((buf[at + 1].toInt() and 0xFF) shl 8)).toShort() - } - 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(): Long = readI32().toLong() and 0xFFFFFFFFL - fun readI64(): Long { - val lo = readI32().toLong() and 0xFFFFFFFFL - val hi = readI32().toLong() - return lo or (hi shl 32) - } - 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) - } - fun readOptional(read: () -> T): T? = if (readBool()) read() else null - fun readList(read: () -> T): List { - val n = readLen() - val out = ArrayList(n) - repeat(n) { out.add(read()) } - return out - } - fun readMap(readKey: () -> K, readValue: () -> V): Map { - val n = readLen() - val out = LinkedHashMap() - repeat(n) { out[readKey()] = readValue() } - return out - } - fun expectEnd() { - if (pos != buf.size) malformed("trailing bytes after value") - } -} - -internal fun weaveEncode(write: (WeaveBufferWriter) -> Unit): ByteArray { - val w = WeaveBufferWriter() - write(w) - return w.toByteArray() -} - -internal fun weaveDecode(bytes: ByteArray, read: (WeaveBufferReader) -> T): T { - val r = WeaveBufferReader(bytes) - val v = read(r) - r.expectEnd() - return v -} - -// END WeaveFFI.kt diff --git a/crates/weaveffi-cli/tests/snapshots/android_kvstore__build_gradle.snap b/crates/weaveffi-cli/tests/snapshots/android_kvstore__build_gradle.snap deleted file mode 100644 index 5cf5c482..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_kvstore__build_gradle.snap +++ /dev/null @@ -1,31 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -plugins { - id 'com.android.library' - id 'org.jetbrains.kotlin.android' version '1.9.22' apply false -} - -android { - namespace 'com.weaveffi' - compileSdk 34 - defaultConfig { - minSdk 24 - externalNativeBuild { - cmake { - cppFlags "" - } - } - } - externalNativeBuild { - cmake { - path "src/main/cpp/CMakeLists.txt" - } - } -} - -// END build.gradle diff --git a/crates/weaveffi-cli/tests/snapshots/android_kvstore__settings_gradle.snap b/crates/weaveffi-cli/tests/snapshots/android_kvstore__settings_gradle.snap deleted file mode 100644 index f10a8b92..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_kvstore__settings_gradle.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -rootProject.name = 'weaveffi' - -// END settings.gradle diff --git a/crates/weaveffi-cli/tests/snapshots/android_kvstore__src_main_cpp_CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/android_kvstore__src_main_cpp_CMakeLists_txt.snap deleted file mode 100644 index e2c5c99f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_kvstore__src_main_cpp_CMakeLists_txt.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -cmake_minimum_required(VERSION 3.22) -project(weaveffi) -add_library(weaveffi SHARED weaveffi_jni.c) -target_include_directories(weaveffi PRIVATE ../../../../c) - -# END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/android_kvstore__src_main_cpp_weaveffi_jni_c.snap b/crates/weaveffi-cli/tests/snapshots/android_kvstore__src_main_cpp_weaveffi_jni_c.snap deleted file mode 100644 index 0fd18c3d..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_kvstore__src_main_cpp_weaveffi_jni_c.snap +++ /dev/null @@ -1,346 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include -#include -#include -#include -#include -#include -#include "weaveffi.h" - -static void throw_weaveffi_error(JNIEnv* env, weaveffi_error* err) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jclass exClass = (*env)->FindClass(env, "com/weaveffi/WeaveFFIException"); - if (exClass != NULL) { - jmethodID ctor = (*env)->GetMethodID(env, exClass, "", "(ILjava/lang/String;)V"); - jstring jmsg = (*env)->NewStringUTF(env, msg); - jthrowable ex = (jthrowable)(*env)->NewObject(env, exClass, ctor, (jint)err->code, jmsg); - if (ex != NULL) { (*env)->Throw(env, ex); } - } - weaveffi_error_clear(err); -} - -static void throw_weaveffi_kv_KvError(JNIEnv* env, weaveffi_error* err) { - jclass exClass = (*env)->FindClass(env, "com/weaveffi/KvException"); - jmethodID fromCode = exClass ? (*env)->GetStaticMethodID(env, exClass, "fromCode", "(ILjava/lang/String;[B)Lcom/weaveffi/WeaveFFIException;") : NULL; - if (fromCode == NULL) { - (*env)->ExceptionClear(env); - throw_weaveffi_error(env, err); - return; - } - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = (*env)->NewStringUTF(env, msg); - jbyteArray jpayload = NULL; - if (err->payload_ptr != NULL) { - jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); - if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } - } - jthrowable ex = (jthrowable)(*env)->CallStaticObjectMethod(env, exClass, fromCode, (jint)err->code, jmsg, jpayload); - if (ex != NULL && !(*env)->ExceptionCheck(env)) { (*env)->Throw(env, ex); } - weaveffi_error_clear(err); -} - -typedef struct { - JavaVM* jvm; - jobject callback; -} weaveffi_jni_async_ctx; - -static jclass weaveffi_jni_entry_class = NULL; -static jmethodID weaveffi_jni_dispatch_exc = NULL; - -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { - (void)reserved; - JNIEnv* env = NULL; - if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } - jclass cls = (*env)->FindClass(env, "com/weaveffi/WeaveFFI"); - if (cls == NULL) { (*env)->ExceptionClear(env); return JNI_VERSION_1_6; } - weaveffi_jni_entry_class = (jclass)(*env)->NewGlobalRef(env, cls); - weaveffi_jni_dispatch_exc = (*env)->GetStaticMethodID(env, weaveffi_jni_entry_class, "dispatchCallbackException", "(Ljava/lang/Throwable;)V"); - if (weaveffi_jni_dispatch_exc == NULL) { (*env)->ExceptionClear(env); } - return JNI_VERSION_1_6; -} - -static void weaveffi_jni_handle_uncaught(JNIEnv* env) { - if (!(*env)->ExceptionCheck(env)) { return; } - jthrowable ex = (*env)->ExceptionOccurred(env); - (*env)->ExceptionClear(env); - if (weaveffi_jni_entry_class != NULL && weaveffi_jni_dispatch_exc != NULL) { - (*env)->CallStaticVoidMethod(env, weaveffi_jni_entry_class, weaveffi_jni_dispatch_exc, ex); - if (!(*env)->ExceptionCheck(env)) { (*env)->DeleteLocalRef(env, ex); return; } - (*env)->ExceptionClear(env); - } - (*env)->Throw(env, ex); - (*env)->ExceptionDescribe(env); - (*env)->ExceptionClear(env); - (*env)->DeleteLocalRef(env, ex); -} - -typedef struct weaveffi_jni_listener_ctx { - JavaVM* jvm; - jobject callback; - uint64_t id; - struct weaveffi_jni_listener_ctx* next; -} weaveffi_jni_listener_ctx; - -static weaveffi_jni_listener_ctx* weaveffi_jni_listeners = NULL; -static pthread_mutex_t weaveffi_jni_listener_lock = PTHREAD_MUTEX_INITIALIZER; - -static void weaveffi_kv_OnEvict_fn_jni_tramp(const char* key, void* context) { - weaveffi_jni_listener_ctx* ctx = (weaveffi_jni_listener_ctx*)context; - JNIEnv* env = NULL; - int attached = 0; - if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { - if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) return; - attached = 1; - } - if ((*env)->PushLocalFrame(env, 32) != 0) { - if (attached) (*ctx->jvm)->DetachCurrentThread(ctx->jvm); - return; - } - jobject _a0 = key ? (jobject)(*env)->NewStringUTF(env, key) : (jobject)(*env)->NewStringUTF(env, ""); - jclass fn_cls = (*env)->GetObjectClass(env, ctx->callback); - jmethodID invoke = (*env)->GetMethodID(env, fn_cls, "invoke", "(Ljava/lang/Object;)Ljava/lang/Object;"); - (*env)->CallObjectMethod(env, ctx->callback, invoke, _a0); - weaveffi_jni_handle_uncaught(env); - (*env)->PopLocalFrame(env, NULL); - if (attached) (*ctx->jvm)->DetachCurrentThread(ctx->jvm); -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_WeaveFFI_registerEvictionListener(JNIEnv* env, jclass clazz, jobject callback) { - weaveffi_jni_listener_ctx* ctx = (weaveffi_jni_listener_ctx*)calloc(1, sizeof(weaveffi_jni_listener_ctx)); - (*env)->GetJavaVM(env, &ctx->jvm); - ctx->callback = (*env)->NewGlobalRef(env, callback); - uint64_t id = weaveffi_kv_register_eviction_listener(weaveffi_kv_OnEvict_fn_jni_tramp, ctx); - ctx->id = id; - pthread_mutex_lock(&weaveffi_jni_listener_lock); - ctx->next = weaveffi_jni_listeners; - weaveffi_jni_listeners = ctx; - pthread_mutex_unlock(&weaveffi_jni_listener_lock); - return (jlong)id; -} - -JNIEXPORT void JNICALL Java_com_weaveffi_WeaveFFI_unregisterEvictionListener(JNIEnv* env, jclass clazz, jlong id) { - weaveffi_kv_unregister_eviction_listener((uint64_t)id); - pthread_mutex_lock(&weaveffi_jni_listener_lock); - weaveffi_jni_listener_ctx** link = &weaveffi_jni_listeners; - while (*link != NULL) { - if ((*link)->id == (uint64_t)id) { - weaveffi_jni_listener_ctx* found = *link; - *link = found->next; - (*env)->DeleteGlobalRef(env, found->callback); - free(found); - break; - } - link = &(*link)->next; - } - pthread_mutex_unlock(&weaveffi_jni_listener_lock); -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_WeaveFFI_getStatsJni(JNIEnv* env, jclass clazz, jlong store) { - weaveffi_error err = {0, NULL, NULL, 0}; - size_t out_len = 0; - const uint8_t* rv = weaveffi_kv_stats_get_stats((const weaveffi_kv_Store*)(intptr_t)store, &out_len, &err); - if (err.code != 0) { - throw_weaveffi_kv_KvError(env, &err); - return NULL; - } - jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len); - if (out && rv) { (*env)->SetByteArrayRegion(env, out, 0, (jsize)out_len, (const jbyte*)rv); } - weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len); - return out; -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_Store_nativeOpen(JNIEnv* env, jclass clazz, jstring path) { - weaveffi_error err = {0, NULL, NULL, 0}; - const char* path_chars = (*env)->GetStringUTFChars(env, path, NULL); - void* rv = weaveffi_kv_Store_open(path_chars, &err); - (*env)->ReleaseStringUTFChars(env, path, path_chars); - if (err.code != 0) { - throw_weaveffi_kv_KvError(env, &err); - return 0; - } - return (jlong)(intptr_t) rv; -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_Store_nativeDefaultCapacity(JNIEnv* env, jclass clazz) { - weaveffi_error err = {0, NULL, NULL, 0}; - int64_t rv = weaveffi_kv_Store_default_capacity(&err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jlong) rv; -} - -JNIEXPORT jboolean JNICALL Java_com_weaveffi_Store_nativePut(JNIEnv* env, jclass clazz, jlong selfHandle, jstring key, jbyteArray value, jint kind, jbyteArray ttl_seconds) { - weaveffi_error err = {0, NULL, NULL, 0}; - const char* key_chars = (*env)->GetStringUTFChars(env, key, NULL); - jboolean value_is_copy = 0; - jbyte* value_elems = (*env)->GetByteArrayElements(env, value, &value_is_copy); - jsize value_len = (*env)->GetArrayLength(env, value); - jbyte* ttl_seconds_elems = (*env)->GetByteArrayElements(env, ttl_seconds, NULL); - jsize ttl_seconds_len = (*env)->GetArrayLength(env, ttl_seconds); - bool rv = weaveffi_kv_Store_put((const weaveffi_kv_Store*)(intptr_t)selfHandle, key_chars, (const uint8_t*)value_elems, (size_t)value_len, (int32_t)kind, (const uint8_t*)ttl_seconds_elems, (size_t)ttl_seconds_len, &err); - (*env)->ReleaseStringUTFChars(env, key, key_chars); - (*env)->ReleaseByteArrayElements(env, value, value_elems, 0); - (*env)->ReleaseByteArrayElements(env, ttl_seconds, ttl_seconds_elems, JNI_ABORT); - if (err.code != 0) { - throw_weaveffi_kv_KvError(env, &err); - return JNI_FALSE; - } - return rv ? JNI_TRUE : JNI_FALSE; -} - -JNIEXPORT jbyteArray JNICALL Java_com_weaveffi_Store_nativeGet(JNIEnv* env, jclass clazz, jlong selfHandle, jstring key) { - weaveffi_error err = {0, NULL, NULL, 0}; - const char* key_chars = (*env)->GetStringUTFChars(env, key, NULL); - size_t out_len = 0; - const uint8_t* rv = weaveffi_kv_Store_get((const weaveffi_kv_Store*)(intptr_t)selfHandle, key_chars, &out_len, &err); - (*env)->ReleaseStringUTFChars(env, key, key_chars); - if (err.code != 0) { - throw_weaveffi_kv_KvError(env, &err); - return NULL; - } - jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len); - if (out && rv) { (*env)->SetByteArrayRegion(env, out, 0, (jsize)out_len, (const jbyte*)rv); } - weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len); - return out; -} - -JNIEXPORT jboolean JNICALL Java_com_weaveffi_Store_nativeDelete(JNIEnv* env, jclass clazz, jlong selfHandle, jstring key) { - weaveffi_error err = {0, NULL, NULL, 0}; - const char* key_chars = (*env)->GetStringUTFChars(env, key, NULL); - bool rv = weaveffi_kv_Store_delete((const weaveffi_kv_Store*)(intptr_t)selfHandle, key_chars, &err); - (*env)->ReleaseStringUTFChars(env, key, key_chars); - if (err.code != 0) { - throw_weaveffi_kv_KvError(env, &err); - return JNI_FALSE; - } - return rv ? JNI_TRUE : JNI_FALSE; -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_Store_nativeListKeys(JNIEnv* env, jclass clazz, jlong selfHandle, jbyteArray prefix) { - weaveffi_error err = {0, NULL, NULL, 0}; - jbyte* prefix_elems = (*env)->GetByteArrayElements(env, prefix, NULL); - jsize prefix_len = (*env)->GetArrayLength(env, prefix); - weaveffi_kv_Store_ListKeysIterator* _iter = weaveffi_kv_Store_list_keys((const weaveffi_kv_Store*)(intptr_t)selfHandle, (const uint8_t*)prefix_elems, (size_t)prefix_len, &err); - (*env)->ReleaseByteArrayElements(env, prefix, prefix_elems, JNI_ABORT); - if (err.code != 0) { - throw_weaveffi_kv_KvError(env, &err); - return 0; - } - return (jlong)(intptr_t)_iter; -} - -JNIEXPORT jlong JNICALL Java_com_weaveffi_Store_nativeCount(JNIEnv* env, jclass clazz, jlong selfHandle) { - weaveffi_error err = {0, NULL, NULL, 0}; - int64_t rv = weaveffi_kv_Store_count((const weaveffi_kv_Store*)(intptr_t)selfHandle, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - return 0; - } - return (jlong) rv; -} - -JNIEXPORT void JNICALL Java_com_weaveffi_Store_nativeClear(JNIEnv* env, jclass clazz, jlong selfHandle) { - weaveffi_error err = {0, NULL, NULL, 0}; - weaveffi_kv_Store_clear((const weaveffi_kv_Store*)(intptr_t)selfHandle, &err); - if (err.code != 0) { - throw_weaveffi_error(env, &err); - - } - return; -} - -static void weaveffi_kv_Store_compact_jni_cb(void* context, weaveffi_error* err, int64_t result) { - weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)context; - JNIEnv* env = NULL; - int attached = 0; - if ((*ctx->jvm)->GetEnv(ctx->jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { - if ((*ctx->jvm)->AttachCurrentThread(ctx->jvm, (void**)&env, NULL) != JNI_OK) { free(ctx); return; } - attached = 1; - } - if (err != NULL && err->code != 0) { - const char* msg = err->message ? err->message : "WeaveFFI error"; - jstring jmsg = (*env)->NewStringUTF(env, msg); - jbyteArray jpayload = NULL; - if (err->payload_ptr != NULL) { - jpayload = (*env)->NewByteArray(env, (jsize)err->payload_len); - if (jpayload != NULL) { (*env)->SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr); } - } - jint jcode = (jint)err->code; - weaveffi_error_free(err); - jclass cls = (*env)->GetObjectClass(env, ctx->callback); - jmethodID mid = (*env)->GetMethodID(env, cls, "onError", "(ILjava/lang/String;[B)V"); - (*env)->CallVoidMethod(env, ctx->callback, mid, jcode, jmsg, jpayload); - } else { - jclass boxCls = (*env)->FindClass(env, "java/lang/Long"); - jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, "valueOf", "(J)Ljava/lang/Long;"); - jobject boxed = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, (jlong)result); - jclass cls = (*env)->GetObjectClass(env, ctx->callback); - jmethodID mid = (*env)->GetMethodID(env, cls, "onSuccess", "(Ljava/lang/Object;)V"); - (*env)->CallVoidMethod(env, ctx->callback, mid, boxed); - } - weaveffi_jni_handle_uncaught(env); - (*env)->DeleteGlobalRef(env, ctx->callback); - JavaVM* jvm = ctx->jvm; - free(ctx); - if (attached) (*jvm)->DetachCurrentThread(jvm); -} - -JNIEXPORT void JNICALL Java_com_weaveffi_Store_nativeCompactAsync(JNIEnv* env, jclass clazz, jlong selfHandle, jlong cancelToken, jobject callback) { - weaveffi_jni_async_ctx* ctx = (weaveffi_jni_async_ctx*)malloc(sizeof(weaveffi_jni_async_ctx)); - (*env)->GetJavaVM(env, &ctx->jvm); - ctx->callback = (*env)->NewGlobalRef(env, callback); - weaveffi_kv_Store_compact_async((const weaveffi_kv_Store*)(intptr_t)selfHandle, (weaveffi_cancel_token*)(intptr_t)cancelToken, weaveffi_kv_Store_compact_jni_cb, ctx); -} - -JNIEXPORT jboolean JNICALL Java_com_weaveffi_Store_nativeLegacyPut(JNIEnv* env, jclass clazz, jlong selfHandle, jstring key, jbyteArray value) { - weaveffi_error err = {0, NULL, NULL, 0}; - const char* key_chars = (*env)->GetStringUTFChars(env, key, NULL); - jboolean value_is_copy = 0; - jbyte* value_elems = (*env)->GetByteArrayElements(env, value, &value_is_copy); - jsize value_len = (*env)->GetArrayLength(env, value); - bool rv = weaveffi_kv_Store_legacy_put((const weaveffi_kv_Store*)(intptr_t)selfHandle, key_chars, (const uint8_t*)value_elems, (size_t)value_len, &err); - (*env)->ReleaseStringUTFChars(env, key, key_chars); - (*env)->ReleaseByteArrayElements(env, value, value_elems, 0); - if (err.code != 0) { - throw_weaveffi_kv_KvError(env, &err); - return JNI_FALSE; - } - return rv ? JNI_TRUE : JNI_FALSE; -} - -JNIEXPORT void JNICALL Java_com_weaveffi_Store_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) { - weaveffi_kv_Store_destroy((weaveffi_kv_Store*)(intptr_t)handle); -} - -JNIEXPORT jobjectArray JNICALL Java_com_weaveffi_KvStoreListKeysIterator_nativeNext(JNIEnv* env, jclass clazz, jlong handle) { - weaveffi_kv_Store_ListKeysIterator* _iter = (weaveffi_kv_Store_ListKeysIterator*)(intptr_t)handle; - const char* _item = (const char*)0; - weaveffi_error err = {0, NULL, NULL, 0}; - int32_t _has = weaveffi_kv_Store_ListKeysIterator_next(_iter, &_item, &err); - if (err.code != 0) { - throw_weaveffi_kv_KvError(env, &err); - return NULL; - } - if (_has == 0) { return NULL; } - jstring _jitem = _item ? (*env)->NewStringUTF(env, _item) : (*env)->NewStringUTF(env, ""); - weaveffi_free_string(_item); - jclass _obj_cls = (*env)->FindClass(env, "java/lang/Object"); - jobjectArray _slot = (*env)->NewObjectArray(env, 1, _obj_cls, NULL); - (*env)->SetObjectArrayElement(env, _slot, 0, _jitem); - return _slot; -} - -JNIEXPORT void JNICALL Java_com_weaveffi_KvStoreListKeysIterator_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle) { - weaveffi_kv_Store_ListKeysIterator_destroy((weaveffi_kv_Store_ListKeysIterator*)(intptr_t)handle); -} - - -// END weaveffi_jni.c diff --git a/crates/weaveffi-cli/tests/snapshots/android_kvstore__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap b/crates/weaveffi-cli/tests/snapshots/android_kvstore__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap deleted file mode 100644 index 2274b6fc..00000000 --- a/crates/weaveffi-cli/tests/snapshots/android_kvstore__src_main_kotlin_com_weaveffi_WeaveFFI_kt.snap +++ /dev/null @@ -1,436 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -package com.weaveffi - -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - -class WeaveFFI { - companion object { - init { System.loadLibrary("weaveffi") } - - @Volatile private var callbackExceptionHandler: ((Throwable) -> Unit)? = null - - /** - * Installs a handler for exceptions thrown by listener callbacks and - * async continuations on native producer threads. These exceptions have - * no Kotlin caller to propagate to; when no handler is installed, they - * are logged with their stack trace and dropped. Pass `null` to - * restore the default logging behavior. - */ - @JvmStatic fun setCallbackExceptionHandler(handler: ((Throwable) -> Unit)?) { - callbackExceptionHandler = handler - } - - // Invoked from the JNI glue; rethrowing signals "no handler" so the - // glue falls back to ExceptionDescribe. - @JvmStatic private fun dispatchCallbackException(t: Throwable) { - callbackExceptionHandler?.invoke(t) ?: throw t - } - - /** - * Subscribe to per-key eviction notifications - * - * Exceptions thrown by the callback cannot propagate to a caller (events fire on a producer thread); they are delivered to the handler installed via [setCallbackExceptionHandler], or logged with their stack trace and dropped when no handler is set. - */ - @JvmStatic external fun registerEvictionListener(callback: (String) -> Unit): Long - @JvmStatic external fun unregisterEvictionListener(id: Long) - /** - * Snapshot the current store statistics - */ - @JvmStatic private external fun getStatsJni(store: Long): ByteArray - @JvmStatic fun getStats(store: Store): Stats = weaveDecode(getStatsJni(store.handle)) { r -> unpackStats(r) } - } -} - -/** Persistence semantics applied to a stored entry */ -enum class EntryKind(val value: Int) { - /** In-memory only; lost on close */ - Volatile(0), - /** Flushed to durable storage */ - Persistent(1), - /** Persistent and encrypted at rest */ - Encrypted(2); - - companion object { - fun fromValue(value: Int): EntryKind = entries.first { it.value == value } - } -} - -/** A single key-value entry persisted in the store */ -data class Entry( - /** Stable monotonic identifier assigned on insert */ - val id: Long, - /** UTF-8 lookup key */ - val key: String, - /** Opaque binary payload */ - val value: ByteArray, - /** Unix-timestamp seconds when the entry was created */ - val created_at: Long, - /** Optional unix-timestamp seconds at which the entry expires */ - val expires_at: Long?, - /** Free-form labels attached to the entry */ - val tags: List, - /** Arbitrary string-valued metadata pairs */ - val metadata: Map, -) - -internal fun packEntry(w: WeaveBufferWriter, v: Entry) { - w.writeI64(v.id) - w.writeString(v.key) - w.writeBytes(v.value) - w.writeI64(v.created_at) - w.writeOptional(v.expires_at) { v0 -> w.writeI64(v0) } - w.writeList(v.tags) { v0 -> w.writeString(v0) } - w.writeMap(v.metadata, { k0 -> w.writeString(k0) }, { v0 -> w.writeString(v0) }) -} - -internal fun unpackEntry(r: WeaveBufferReader): Entry = Entry( - r.readI64(), - r.readString(), - r.readBytes(), - r.readI64(), - r.readOptional { r.readI64() }, - r.readList { r.readString() }, - r.readMap({ r.readString() }, { r.readString() }), -) - -/** An in-memory key-value store owning its entries; destroying the object releases all of its state */ -class Store internal constructor(internal var handle: Long) : java.io.Closeable { - companion object { - init { System.loadLibrary("weaveffi") } - - @JvmStatic private external fun nativeOpen(path: String): Long - @JvmStatic private external fun nativeDefaultCapacity(): Long - @JvmStatic private external fun nativePut(selfHandle: Long, key: String, value: ByteArray, kind: Int, ttlSeconds: ByteArray): Boolean - @JvmStatic private external fun nativeGet(selfHandle: Long, key: String): ByteArray - @JvmStatic private external fun nativeDelete(selfHandle: Long, key: String): Boolean - @JvmStatic private external fun nativeListKeys(selfHandle: Long, prefix: ByteArray): Long - @JvmStatic private external fun nativeCount(selfHandle: Long): Long - @JvmStatic private external fun nativeClear(selfHandle: Long): Unit - @JvmStatic private external fun nativeCompactAsync(selfHandle: Long, cancelToken: Long, callback: Any) - @JvmStatic private external fun nativeLegacyPut(selfHandle: Long, key: String, value: ByteArray): Boolean - @JvmStatic private external fun nativeDestroy(handle: Long) - - /** - * Open (or create) a store backed by the given filesystem path - */ - fun open(path: String): Store = Store(nativeOpen(path)) - - /** - * The largest number of live entries one store will hold - */ - fun defaultCapacity(): Long = nativeDefaultCapacity() - } - - /** - * Insert or replace a value, returning true on success - */ - fun put(key: String, value: ByteArray, kind: EntryKind, ttlSeconds: Long?): Boolean = nativePut(handle, key, value, kind.value, weaveEncode { w -> w.writeOptional(ttlSeconds) { v0 -> w.writeI64(v0) } }) - - /** - * Look up an entry by key; returns null if missing or expired - */ - fun get(key: String): Entry? = weaveDecode(nativeGet(handle, key)) { r -> r.readOptional { unpackEntry(r) } } - - /** - * Remove the entry for the given key, returning true if it existed - */ - fun delete(key: String): Boolean = nativeDelete(handle, key) - - /** - * Stream every key, optionally filtered by a prefix - */ - fun listKeys(prefix: String?): Iterator = KvStoreListKeysIterator(nativeListKeys(handle, weaveEncode { w -> w.writeOptional(prefix) { v0 -> w.writeString(v0) } })) - - /** - * Return the number of live entries in the store - */ - fun count(): Long = nativeCount(handle) - - /** - * Drop every entry from the store - */ - fun clear() { nativeClear(handle) } - - /** - * Reclaim space asynchronously; returns the number of bytes reclaimed - */ - suspend fun compact(): Long = suspendCancellableCoroutine { cont -> - nativeCompactAsync(handle, 0L, WeaveContinuation(cont) { code, message, payload -> KvException.fromCode(code, message, payload) }) - } - - /** - * Legacy single-shot put kept for compatibility - */ - @Deprecated("use put() with explicit kind") - fun legacyPut(key: String, value: ByteArray): Boolean = nativeLegacyPut(handle, key, value) - - override fun close() { - if (handle != 0L) { - nativeDestroy(handle) - handle = 0L - } - } - - protected fun finalize() { - close() - } -} - -/** - * A lazy iterator over the `String` elements streamed by [listKeys]. Each step pulls - * exactly one element from the native producer. The native handle is - * released when the producer is exhausted, when [close] is called, or by - * the finalizer if the iterator is abandoned, whichever comes first. - */ -class KvStoreListKeysIterator internal constructor(private var handle: Long) : Iterator, java.io.Closeable { - private var nextSlot: Array? = null - - override fun hasNext(): Boolean { - if (nextSlot != null) return true - if (handle == 0L) return false - val slot = nativeNext(handle) - if (slot == null) { - close() - return false - } - nextSlot = slot - return true - } - - override fun next(): String { - if (!hasNext()) throw NoSuchElementException() - val raw = nextSlot!![0] - nextSlot = null - return raw as String - } - - override fun close() { - if (handle != 0L) { - nativeDestroy(handle) - handle = 0L - } - } - - protected fun finalize() { - close() - } - - companion object { - init { System.loadLibrary("weaveffi") } - - @JvmStatic private external fun nativeNext(handle: Long): Array? - @JvmStatic private external fun nativeDestroy(handle: Long) - } -} - -/** Aggregate store statistics */ -data class Stats( - /** Number of live entries */ - val total_entries: Long, - /** Sum of all value byte lengths */ - val total_bytes: Long, - /** Number of entries past their TTL but not yet evicted */ - val expired_entries: Long, -) - -internal fun packStats(w: WeaveBufferWriter, v: Stats) { - w.writeI64(v.total_entries) - w.writeI64(v.total_bytes) - w.writeI64(v.expired_entries) -} - -internal fun unpackStats(r: WeaveBufferReader): Stats = Stats( - r.readI64(), - r.readI64(), - r.readI64(), -) - -/** Generic WeaveFFI failure: panics, marshalling errors, and unknown codes. */ -open class WeaveFFIException(val code: Int, message: String) : Exception(message) - -/** Typed error domain `KvError` declared by module `kv`. */ -sealed class KvException(code: Int, message: String) : WeaveFFIException(code, message) { - /** No entry exists for the requested key */ - class KeyNotFound(message: String = "key not found") : KvException(1001, message) - /** The entry exists but its TTL has elapsed */ - class Expired(message: String = "entry expired") : KvException(1002, message) - /** The store has reached its capacity limit */ - class StoreFull(message: String = "store has reached capacity") : KvException(1003, message) - /** Underlying storage reported an I/O failure */ - class IoError(message: String = "I/O failure") : KvException(1004, message) - - companion object { - /** Map a raw `KvError` code and payload to the typed exception; unknown codes yield the generic [WeaveFFIException]. */ - @JvmStatic fun fromCode(code: Int, message: String, payload: ByteArray?): WeaveFFIException = when (code) { - 1001 -> KeyNotFound(message) - 1002 -> Expired(message) - 1003 -> StoreFull(message) - 1004 -> IoError(message) - else -> WeaveFFIException(code, message) - } - } -} - -/** Growable little-endian writer implementing the WeaveFFI value-buffer wire format. */ -internal class WeaveBufferWriter { - private var buf = ByteArray(32) - private var len = 0 - - 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 writeI16(v: Short) { - reserve(2) - val b = v.toInt() - buf[len++] = (b and 0xFF).toByte() - buf[len++] = ((b shr 8) and 0xFF).toByte() - } - fun writeI32(v: Int) { - reserve(4) - buf[len++] = (v and 0xFF).toByte() - buf[len++] = ((v shr 8) and 0xFF).toByte() - buf[len++] = ((v shr 16) and 0xFF).toByte() - buf[len++] = ((v shr 24) and 0xFF).toByte() - } - fun writeU32(v: Long) = writeI32(v.toInt()) - fun writeI64(v: Long) { - writeI32(v.toInt()) - writeI32((v ushr 32).toInt()) - } - fun writeF32(v: Float) = writeI32(v.toRawBits()) - fun writeF64(v: Double) = writeI64(v.toRawBits()) - fun writeString(v: String) = writeBytes(v.toByteArray(Charsets.UTF_8)) - fun writeBytes(v: ByteArray) { - writeI32(v.size) - reserve(v.size) - v.copyInto(buf, len) - len += v.size - } - fun writeOptional(v: T?, write: (T) -> Unit) { - if (v == null) writeBool(false) else { writeBool(true); write(v) } - } - fun writeList(v: List, write: (T) -> Unit) { - writeI32(v.size) - for (e in v) write(e) - } - 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 WeaveFFI value-buffer wire format. */ -internal class WeaveBufferReader(private val buf: ByteArray) { - private var pos = 0 - - private fun malformed(detail: String): Nothing = - throw WeaveFFIException(-2, "malformed WeaveFFI 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 readI16(): Short { - val at = take(2) - return ((buf[at].toInt() and 0xFF) or ((buf[at + 1].toInt() and 0xFF) shl 8)).toShort() - } - 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(): Long = readI32().toLong() and 0xFFFFFFFFL - fun readI64(): Long { - val lo = readI32().toLong() and 0xFFFFFFFFL - val hi = readI32().toLong() - return lo or (hi shl 32) - } - 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) - } - fun readOptional(read: () -> T): T? = if (readBool()) read() else null - fun readList(read: () -> T): List { - val n = readLen() - val out = ArrayList(n) - repeat(n) { out.add(read()) } - return out - } - fun readMap(readKey: () -> K, readValue: () -> V): Map { - val n = readLen() - val out = LinkedHashMap() - repeat(n) { out[readKey()] = readValue() } - return out - } - fun expectEnd() { - if (pos != buf.size) malformed("trailing bytes after value") - } -} - -internal fun weaveEncode(write: (WeaveBufferWriter) -> Unit): ByteArray { - val w = WeaveBufferWriter() - write(w) - return w.toByteArray() -} - -internal fun weaveDecode(bytes: ByteArray, read: (WeaveBufferReader) -> T): T { - val r = WeaveBufferReader(bytes) - val v = read(r) - r.expectEnd() - return v -} - -internal class WeaveContinuation( - private val cont: kotlinx.coroutines.CancellableContinuation, - private val mapError: (Int, String, ByteArray?) -> Throwable -) { - @Suppress("UNCHECKED_CAST") - fun onSuccess(result: Any?) { cont.resume(result as T) } - fun onError(code: Int, message: String, payload: ByteArray?) { cont.resumeWithException(mapError(code, message, payload)) } -} - -// END WeaveFFI.kt diff --git a/crates/weaveffi-cli/tests/snapshots/c_async_demo__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_async_demo__weaveffi_h.snap deleted file mode 100644 index 9cea224e..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_async_demo__weaveffi_h.snap +++ /dev/null @@ -1,112 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#ifndef WEAVEFFI_H -#define WEAVEFFI_H - -#include -#include -#include - -#ifndef WEAVEFFI_API -# if defined(_WIN32) || defined(__CYGWIN__) -# ifdef WEAVEFFI_BUILD -# define WEAVEFFI_API __declspec(dllexport) -# else -# define WEAVEFFI_API __declspec(dllimport) -# endif -# elif defined(__EMSCRIPTEN__) -# define WEAVEFFI_API __attribute__((used, visibility("default"))) -# elif defined(__GNUC__) && (__GNUC__ >= 4) -# define WEAVEFFI_API __attribute__((visibility("default"))) -# else -# define WEAVEFFI_API -# endif -#endif - -#ifndef WEAVEFFI_DEPRECATED -# if defined(_MSC_VER) -# define WEAVEFFI_DEPRECATED(msg) __declspec(deprecated(msg)) -# elif defined(__GNUC__) || defined(__clang__) -# define WEAVEFFI_DEPRECATED(msg) __attribute__((deprecated(msg))) -# else -# define WEAVEFFI_DEPRECATED(msg) -# endif -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -typedef uint64_t weaveffi_handle_t; - -/* Error slot written by every fallible call. `payload_ptr`/`payload_len` - hold the matched error code's fields serialized in the WeaveFFI value - buffer format (null when the code declares no fields); both the message - and the payload are released by weaveffi_error_clear. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); - -/* Async completion callbacks receive a heap-boxed error the consumer - owns; weaveffi_error_free releases the message, the payload, and the - box itself. Passing NULL is a safe no-op. */ -WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); -WEAVEFFI_API void weaveffi_free_string(const char* ptr); -WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); - -/* Linear-memory allocator used by the Wasm JS glue to stage call - arguments. Native consumers never call these; producers targeting - WebAssembly must export them (the generated weaveffi.c provides - malloc/free-backed defaults). */ -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size); -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size); - -typedef struct weaveffi_cancel_token weaveffi_cancel_token; -WEAVEFFI_API weaveffi_cancel_token* weaveffi_cancel_token_create(void); -WEAVEFFI_API void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); -WEAVEFFI_API bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); -WEAVEFFI_API void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); - -/* - * Value buffer convention: records, rich enums, lists, maps, and - * optionals cross the ABI serialized in the WeaveFFI value buffer - * format (little-endian; see the generated bindings or the WeaveFFI - * buffer protocol spec for the per-type encoding). - * A buffered parameter named "v" expands to a borrowed view: - * const uint8_t* v_ptr, size_t v_len - * A buffered return is a producer-allocated buffer returned as - * `const uint8_t*` with a trailing `size_t* out_len`; the caller - * decodes it and then releases it with weaveffi_free_bytes. - */ - -/** Error codes reported by throwing functions in the `tasks` module tree. */ -typedef enum { weaveffi_tasks_TaskError_InvalidName = 1 } weaveffi_tasks_TaskError; -typedef void (*weaveffi_tasks_run_task_callback)(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len); -typedef void (*weaveffi_tasks_run_batch_callback)(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len); -typedef void (*weaveffi_tasks_run_n_tasks_callback)(void* context, weaveffi_error* err, int32_t result); - -// Module: tasks -WEAVEFFI_API void weaveffi_tasks_run_task_async(const char* name, weaveffi_tasks_run_task_callback callback, void* context); -WEAVEFFI_API void weaveffi_tasks_run_batch_async(const uint8_t* names_ptr, size_t names_len, weaveffi_tasks_run_batch_callback callback, void* context); -WEAVEFFI_API bool weaveffi_tasks_cancel_task(int64_t id, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_tasks_run_n_tasks_async(int32_t n, weaveffi_tasks_run_n_tasks_callback callback, void* context); -WEAVEFFI_API int64_t weaveffi_tasks_active_callbacks(weaveffi_error* out_err); - - -#ifdef __cplusplus -} -#endif - -#endif // WEAVEFFI_H - -// END weaveffi.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_calculator__weaveffi_c.snap b/crates/weaveffi-cli/tests/snapshots/c_calculator__weaveffi_c.snap deleted file mode 100644 index d88de6a6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_calculator__weaveffi_c.snap +++ /dev/null @@ -1,27 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include "weaveffi.h" - -#include - -// Default implementations of the linear-memory allocator declared in -// weaveffi.h. The Wasm JS glue stages call arguments through these; a -// producer that ships its own allocator can omit this file from the -// build. The size is unused on dealloc because free() recovers it. -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size) { - return (uint8_t*)malloc(size ? size : 1u); -} - -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size) { - (void)size; - free(ptr); -} - -// Optional convenience wrappers can be added here in future versions. - -// END weaveffi.c diff --git a/crates/weaveffi-cli/tests/snapshots/c_calculator__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_calculator__weaveffi_h.snap deleted file mode 100644 index 9fa12951..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_calculator__weaveffi_h.snap +++ /dev/null @@ -1,112 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#ifndef WEAVEFFI_H -#define WEAVEFFI_H - -#include -#include -#include - -#ifndef WEAVEFFI_API -# if defined(_WIN32) || defined(__CYGWIN__) -# ifdef WEAVEFFI_BUILD -# define WEAVEFFI_API __declspec(dllexport) -# else -# define WEAVEFFI_API __declspec(dllimport) -# endif -# elif defined(__EMSCRIPTEN__) -# define WEAVEFFI_API __attribute__((used, visibility("default"))) -# elif defined(__GNUC__) && (__GNUC__ >= 4) -# define WEAVEFFI_API __attribute__((visibility("default"))) -# else -# define WEAVEFFI_API -# endif -#endif - -#ifndef WEAVEFFI_DEPRECATED -# if defined(_MSC_VER) -# define WEAVEFFI_DEPRECATED(msg) __declspec(deprecated(msg)) -# elif defined(__GNUC__) || defined(__clang__) -# define WEAVEFFI_DEPRECATED(msg) __attribute__((deprecated(msg))) -# else -# define WEAVEFFI_DEPRECATED(msg) -# endif -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -typedef uint64_t weaveffi_handle_t; - -/* Error slot written by every fallible call. `payload_ptr`/`payload_len` - hold the matched error code's fields serialized in the WeaveFFI value - buffer format (null when the code declares no fields); both the message - and the payload are released by weaveffi_error_clear. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); - -/* Async completion callbacks receive a heap-boxed error the consumer - owns; weaveffi_error_free releases the message, the payload, and the - box itself. Passing NULL is a safe no-op. */ -WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); -WEAVEFFI_API void weaveffi_free_string(const char* ptr); -WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); - -/* Linear-memory allocator used by the Wasm JS glue to stage call - arguments. Native consumers never call these; producers targeting - WebAssembly must export them (the generated weaveffi.c provides - malloc/free-backed defaults). */ -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size); -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size); - -typedef struct weaveffi_cancel_token weaveffi_cancel_token; -WEAVEFFI_API weaveffi_cancel_token* weaveffi_cancel_token_create(void); -WEAVEFFI_API void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); -WEAVEFFI_API bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); -WEAVEFFI_API void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); - -/* - * Value buffer convention: records, rich enums, lists, maps, and - * optionals cross the ABI serialized in the WeaveFFI value buffer - * format (little-endian; see the generated bindings or the WeaveFFI - * buffer protocol spec for the per-type encoding). - * A buffered parameter named "v" expands to a borrowed view: - * const uint8_t* v_ptr, size_t v_len - * A buffered return is a producer-allocated buffer returned as - * `const uint8_t*` with a trailing `size_t* out_len`; the caller - * decodes it and then releases it with weaveffi_free_bytes. - */ - -/** Error codes reported by throwing functions in the `calculator` module tree. */ -typedef enum { weaveffi_calculator_CalcError_DivisionByZero = 1 } weaveffi_calculator_CalcError; - -// Module: calculator -/** Add two integers */ -WEAVEFFI_API int32_t weaveffi_calculator_add(int32_t a, int32_t b, weaveffi_error* out_err); -/** Multiply two integers */ -WEAVEFFI_API int32_t weaveffi_calculator_mul(int32_t a, int32_t b, weaveffi_error* out_err); -/** Divide two integers, failing on a zero divisor */ -WEAVEFFI_API int32_t weaveffi_calculator_div(int32_t a, int32_t b, weaveffi_error* out_err); -/** Echo a string back */ -WEAVEFFI_API const char* weaveffi_calculator_echo(const char* s, weaveffi_error* out_err); - - -#ifdef __cplusplus -} -#endif - -#endif // WEAVEFFI_H - -// END weaveffi.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_contacts__weaveffi_c.snap b/crates/weaveffi-cli/tests/snapshots/c_contacts__weaveffi_c.snap deleted file mode 100644 index d88de6a6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_contacts__weaveffi_c.snap +++ /dev/null @@ -1,27 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include "weaveffi.h" - -#include - -// Default implementations of the linear-memory allocator declared in -// weaveffi.h. The Wasm JS glue stages call arguments through these; a -// producer that ships its own allocator can omit this file from the -// build. The size is unused on dealloc because free() recovers it. -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size) { - return (uint8_t*)malloc(size ? size : 1u); -} - -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size) { - (void)size; - free(ptr); -} - -// Optional convenience wrappers can be added here in future versions. - -// END weaveffi.c diff --git a/crates/weaveffi-cli/tests/snapshots/c_contacts__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_contacts__weaveffi_h.snap deleted file mode 100644 index ecd6b856..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_contacts__weaveffi_h.snap +++ /dev/null @@ -1,115 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#ifndef WEAVEFFI_H -#define WEAVEFFI_H - -#include -#include -#include - -#ifndef WEAVEFFI_API -# if defined(_WIN32) || defined(__CYGWIN__) -# ifdef WEAVEFFI_BUILD -# define WEAVEFFI_API __declspec(dllexport) -# else -# define WEAVEFFI_API __declspec(dllimport) -# endif -# elif defined(__EMSCRIPTEN__) -# define WEAVEFFI_API __attribute__((used, visibility("default"))) -# elif defined(__GNUC__) && (__GNUC__ >= 4) -# define WEAVEFFI_API __attribute__((visibility("default"))) -# else -# define WEAVEFFI_API -# endif -#endif - -#ifndef WEAVEFFI_DEPRECATED -# if defined(_MSC_VER) -# define WEAVEFFI_DEPRECATED(msg) __declspec(deprecated(msg)) -# elif defined(__GNUC__) || defined(__clang__) -# define WEAVEFFI_DEPRECATED(msg) __attribute__((deprecated(msg))) -# else -# define WEAVEFFI_DEPRECATED(msg) -# endif -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -typedef uint64_t weaveffi_handle_t; - -/* Error slot written by every fallible call. `payload_ptr`/`payload_len` - hold the matched error code's fields serialized in the WeaveFFI value - buffer format (null when the code declares no fields); both the message - and the payload are released by weaveffi_error_clear. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); - -/* Async completion callbacks receive a heap-boxed error the consumer - owns; weaveffi_error_free releases the message, the payload, and the - box itself. Passing NULL is a safe no-op. */ -WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); -WEAVEFFI_API void weaveffi_free_string(const char* ptr); -WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); - -/* Linear-memory allocator used by the Wasm JS glue to stage call - arguments. Native consumers never call these; producers targeting - WebAssembly must export them (the generated weaveffi.c provides - malloc/free-backed defaults). */ -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size); -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size); - -typedef struct weaveffi_cancel_token weaveffi_cancel_token; -WEAVEFFI_API weaveffi_cancel_token* weaveffi_cancel_token_create(void); -WEAVEFFI_API void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); -WEAVEFFI_API bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); -WEAVEFFI_API void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); - -/* - * Value buffer convention: records, rich enums, lists, maps, and - * optionals cross the ABI serialized in the WeaveFFI value buffer - * format (little-endian; see the generated bindings or the WeaveFFI - * buffer protocol spec for the per-type encoding). - * A buffered parameter named "v" expands to a borrowed view: - * const uint8_t* v_ptr, size_t v_len - * A buffered return is a producer-allocated buffer returned as - * `const uint8_t*` with a trailing `size_t* out_len`; the caller - * decodes it and then releases it with weaveffi_free_bytes. - */ - -typedef enum { weaveffi_contacts_ContactType_Personal = 0, weaveffi_contacts_ContactType_Work = 1, weaveffi_contacts_ContactType_Other = 2 } weaveffi_contacts_ContactType; -/** Error codes reported by throwing functions in the `contacts` module tree. */ -typedef enum { weaveffi_contacts_ContactsError_InvalidName = 1, weaveffi_contacts_ContactsError_NotFound = 2 } weaveffi_contacts_ContactsError; -typedef struct weaveffi_contacts_ContactBook weaveffi_contacts_ContactBook; - -// Module: contacts -/** An in-memory address book owning its contacts */ -WEAVEFFI_API weaveffi_contacts_ContactBook* weaveffi_contacts_ContactBook_new(weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_contacts_ContactBook_add(const weaveffi_contacts_ContactBook* self, const char* first_name, const char* last_name, const uint8_t* email_ptr, size_t email_len, weaveffi_contacts_ContactType contact_type, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_contacts_ContactBook_get(const weaveffi_contacts_ContactBook* self, int64_t id, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_contacts_ContactBook_list(const weaveffi_contacts_ContactBook* self, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API bool weaveffi_contacts_ContactBook_remove(const weaveffi_contacts_ContactBook* self, int64_t id, weaveffi_error* out_err); -WEAVEFFI_API int32_t weaveffi_contacts_ContactBook_count(const weaveffi_contacts_ContactBook* self, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_contacts_ContactBook_destroy(weaveffi_contacts_ContactBook* self); - - - -#ifdef __cplusplus -} -#endif - -#endif // WEAVEFFI_H - -// END weaveffi.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__weaveffi_h.snap index 3296d149..1aa6311a 100644 --- a/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__weaveffi_h.snap +++ b/crates/weaveffi-cli/tests/snapshots/c_docs_everywhere__weaveffi_h.snap @@ -42,6 +42,13 @@ source: crates/weaveffi-cli/tests/snapshots.rs extern "C" { #endif +/* The WeaveFFI C ABI revision this header was generated against. The + producer exports weaveffi_abi_version() so a consumer can refuse to load + a library built for a different revision instead of misreading its + error struct or value buffers. */ +#define WEAVEFFI_ABI_VERSION 1u +WEAVEFFI_API uint32_t weaveffi_abi_version(void); + typedef uint64_t weaveffi_handle_t; /* Error slot written by every fallible call. `payload_ptr`/`payload_len` diff --git a/crates/weaveffi-cli/tests/snapshots/c_async_demo__weaveffi_c.snap b/crates/weaveffi-cli/tests/snapshots/c_edge_cases__weaveffi_c.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/c_async_demo__weaveffi_c.snap rename to crates/weaveffi-cli/tests/snapshots/c_edge_cases__weaveffi_c.snap diff --git a/crates/weaveffi-cli/tests/snapshots/c_edge_cases__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_edge_cases__weaveffi_h.snap new file mode 100644 index 00000000..4652fefa --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/c_edge_cases__weaveffi_h.snap @@ -0,0 +1,192 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from weaveffi.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate weaveffi.yml -o + +#ifndef WEAVEFFI_H +#define WEAVEFFI_H + +#include +#include +#include + +#ifndef WEAVEFFI_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef WEAVEFFI_BUILD +# define WEAVEFFI_API __declspec(dllexport) +# else +# define WEAVEFFI_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define WEAVEFFI_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define WEAVEFFI_API __attribute__((visibility("default"))) +# else +# define WEAVEFFI_API +# endif +#endif + +#ifndef WEAVEFFI_DEPRECATED +# if defined(_MSC_VER) +# define WEAVEFFI_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define WEAVEFFI_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define WEAVEFFI_DEPRECATED(msg) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* The WeaveFFI C ABI revision this header was generated against. The + producer exports weaveffi_abi_version() so a consumer can refuse to load + a library built for a different revision instead of misreading its + error struct or value buffers. */ +#define WEAVEFFI_ABI_VERSION 1u +WEAVEFFI_API uint32_t weaveffi_abi_version(void); + +typedef uint64_t weaveffi_handle_t; + +/* Error slot written by every fallible call. `payload_ptr`/`payload_len` + hold the matched error code's fields serialized in the WeaveFFI value + buffer format (null when the code declares no fields); both the message + and the payload are released by weaveffi_error_clear. */ +typedef struct weaveffi_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} weaveffi_error; + +WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); +WEAVEFFI_API void weaveffi_free_string(const char* ptr); +WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); + +/* Linear-memory allocator used by the Wasm JS glue to stage call + arguments. Native consumers never call these; producers targeting + WebAssembly must export them (the generated weaveffi.c provides + malloc/free-backed defaults). */ +WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size); +WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size); + +typedef struct weaveffi_cancel_token weaveffi_cancel_token; +WEAVEFFI_API weaveffi_cancel_token* weaveffi_cancel_token_create(void); +WEAVEFFI_API void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); +WEAVEFFI_API bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); +WEAVEFFI_API void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); + +/* + * Value buffer convention: records, rich enums, lists, maps, and + * optionals cross the ABI serialized in the WeaveFFI value buffer + * format (little-endian; see the generated bindings or the WeaveFFI + * buffer protocol spec for the per-type encoding). + * A buffered parameter named "v" expands to a borrowed view: + * const uint8_t* v_ptr, size_t v_len + * A buffered return is a producer-allocated buffer returned as + * `const uint8_t*` with a trailing `size_t* out_len`; the caller + * decodes it and then releases it with weaveffi_free_bytes. + */ + +/** Deprecated enum with non-contiguous values and keyword-like variants */ +typedef enum { weaveffi_edge_cases_Legacy_None = 0, weaveffi_edge_cases_Legacy_Default = 5, weaveffi_edge_cases_Legacy_Class = 10 } weaveffi_edge_cases_Legacy; +typedef enum { weaveffi_edge_cases_Mode_Fast = 1, weaveffi_edge_cases_Mode_Safe = 2 } weaveffi_edge_cases_Mode; +/** Error codes reported by throwing functions in the `edge.cases` module tree. */ +typedef enum { weaveffi_edge_cases_EdgeErrors_Busy = 7 } weaveffi_edge_cases_EdgeErrors; +typedef struct weaveffi_edge_cases_Widget weaveffi_edge_cases_Widget; +typedef struct weaveffi_edge_cases_Vault weaveffi_edge_cases_Vault; +typedef struct weaveffi_edge_cases_StringStreamIterator weaveffi_edge_cases_StringStreamIterator; +typedef struct weaveffi_edge_cases_ScalarStreamIterator weaveffi_edge_cases_ScalarStreamIterator; +typedef struct weaveffi_edge_cases_ModeStreamIterator weaveffi_edge_cases_ModeStreamIterator; +typedef struct weaveffi_edge_cases_Leaf weaveffi_edge_cases_Leaf; +/** Callback carrying composite payloads */ +typedef void (*weaveffi_edge_cases_OnBatch_fn)(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, weaveffi_edge_cases_Mode mode, void* context); +typedef void (*weaveffi_edge_cases_OnTick_fn)(void* context); +typedef void (*weaveffi_edge_cases_async_void_callback)(void* context, weaveffi_error* err); +typedef void (*weaveffi_edge_cases_async_bytes_callback)(void* context, weaveffi_error* err, const uint8_t* result, size_t result_len); +typedef void (*weaveffi_edge_cases_async_struct_callback)(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*weaveffi_edge_cases_async_list_callback)(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*weaveffi_edge_cases_async_optional_callback)(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*weaveffi_edge_cases_async_enum_callback)(void* context, weaveffi_error* err, weaveffi_edge_cases_Mode result); +typedef void (*weaveffi_edge_cases_async_keywords_callback)(void* context, weaveffi_error* err, const char* result); +typedef void (*weaveffi_edge_cases_Widget_fetch_callback)(void* context, weaveffi_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 */ +WEAVEFFI_API void weaveffi_edge_cases_Widget_refresh(const weaveffi_edge_cases_Widget* self, weaveffi_error* out_err); +WEAVEFFI_API const char* weaveffi_edge_cases_Widget_rename(const weaveffi_edge_cases_Widget* self, const char* class_, const uint8_t* import_ptr, size_t import_len, weaveffi_error* out_err); +/** Async method with a struct result */ +WEAVEFFI_API void weaveffi_edge_cases_Widget_fetch_async(const weaveffi_edge_cases_Widget* self, int64_t id, weaveffi_edge_cases_Widget_fetch_callback callback, void* context); +WEAVEFFI_DEPRECATED("Use rename instead") +WEAVEFFI_API const char* weaveffi_edge_cases_Widget_old_name(const weaveffi_edge_cases_Widget* self, weaveffi_error* out_err); +WEAVEFFI_API void weaveffi_edge_cases_Widget_destroy(weaveffi_edge_cases_Widget* self); + +/** Deprecated interface with a throwing constructor */ +/** Fails when the vault is busy */ +WEAVEFFI_API weaveffi_edge_cases_Vault* weaveffi_edge_cases_Vault_open(const char* path, weaveffi_error* out_err); +/** Throwing static */ +WEAVEFFI_API bool weaveffi_edge_cases_Vault_probe(weaveffi_error* out_err); +WEAVEFFI_API weaveffi_edge_cases_Widget* weaveffi_edge_cases_Vault_peek(const weaveffi_edge_cases_Vault* self, weaveffi_error* out_err); +WEAVEFFI_API void weaveffi_edge_cases_Vault_destroy(weaveffi_edge_cases_Vault* self); + +WEAVEFFI_API uint64_t weaveffi_edge_cases_register_batch_listener(weaveffi_edge_cases_OnBatch_fn callback, void* context); +WEAVEFFI_API void weaveffi_edge_cases_unregister_batch_listener(uint64_t id); +WEAVEFFI_API uint64_t weaveffi_edge_cases_register_tick_listener(weaveffi_edge_cases_OnTick_fn callback, void* context); +WEAVEFFI_API void weaveffi_edge_cases_unregister_tick_listener(uint64_t id); +/** No parameters and no result */ +WEAVEFFI_API void weaveffi_edge_cases_noop(weaveffi_error* out_err); +/** Function named after a keyword whose parameters are all keywords */ +WEAVEFFI_API int32_t weaveffi_edge_cases_select(int32_t class_, const char* from, const uint8_t* func_ptr, size_t func_len, const uint8_t* range_ptr, size_t range_len, bool type_, weaveffi_error* out_err); +/** Optional list of optional structs, the deepest legal nesting */ +WEAVEFFI_API const uint8_t* weaveffi_edge_cases_nested_optional(const uint8_t* items_ptr, size_t items_len, size_t* out_len, weaveffi_error* out_err); +WEAVEFFI_API const uint8_t* weaveffi_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, weaveffi_error* out_err); +/** Write-back parameters */ +WEAVEFFI_API void weaveffi_edge_cases_mutable_params(char* text, uint8_t* blob_ptr, size_t blob_len, weaveffi_error* out_err); +/** Factory for an interface with no constructor */ +WEAVEFFI_API weaveffi_edge_cases_Widget* weaveffi_edge_cases_make_widget(const char* label, weaveffi_error* out_err); +/** Optional interface return */ +WEAVEFFI_API weaveffi_edge_cases_Widget* weaveffi_edge_cases_find_widget(int64_t id, weaveffi_error* out_err); +/** Interface parameter, including an optional one */ +WEAVEFFI_API const char* weaveffi_edge_cases_inspect_widget(const weaveffi_edge_cases_Widget* widget, const weaveffi_edge_cases_Widget* fallback, weaveffi_error* out_err); +WEAVEFFI_API weaveffi_edge_cases_Leaf* weaveffi_edge_cases_typed_handle_param(weaveffi_edge_cases_Leaf* token, weaveffi_handle_t raw, weaveffi_error* out_err); +/** Iterator of strings */ +WEAVEFFI_API weaveffi_edge_cases_StringStreamIterator* weaveffi_edge_cases_string_stream(weaveffi_error* out_err); +WEAVEFFI_API int32_t weaveffi_edge_cases_StringStreamIterator_next(weaveffi_edge_cases_StringStreamIterator* iter, const char** out_item, weaveffi_error* out_err); +WEAVEFFI_API void weaveffi_edge_cases_StringStreamIterator_destroy(weaveffi_edge_cases_StringStreamIterator* iter); +/** Iterator of scalars */ +WEAVEFFI_API weaveffi_edge_cases_ScalarStreamIterator* weaveffi_edge_cases_scalar_stream(weaveffi_error* out_err); +WEAVEFFI_API int32_t weaveffi_edge_cases_ScalarStreamIterator_next(weaveffi_edge_cases_ScalarStreamIterator* iter, int32_t* out_item, weaveffi_error* out_err); +WEAVEFFI_API void weaveffi_edge_cases_ScalarStreamIterator_destroy(weaveffi_edge_cases_ScalarStreamIterator* iter); +/** Iterator of enums */ +WEAVEFFI_API weaveffi_edge_cases_ModeStreamIterator* weaveffi_edge_cases_mode_stream(weaveffi_error* out_err); +WEAVEFFI_API int32_t weaveffi_edge_cases_ModeStreamIterator_next(weaveffi_edge_cases_ModeStreamIterator* iter, weaveffi_edge_cases_Mode* out_item, weaveffi_error* out_err); +WEAVEFFI_API void weaveffi_edge_cases_ModeStreamIterator_destroy(weaveffi_edge_cases_ModeStreamIterator* iter); +/** Async with no result */ +WEAVEFFI_API void weaveffi_edge_cases_async_void_async(weaveffi_edge_cases_async_void_callback callback, void* context); +WEAVEFFI_API void weaveffi_edge_cases_async_bytes_async(uint32_t size, weaveffi_edge_cases_async_bytes_callback callback, void* context); +WEAVEFFI_API void weaveffi_edge_cases_async_struct_async(weaveffi_edge_cases_async_struct_callback callback, void* context); +WEAVEFFI_API void weaveffi_edge_cases_async_list_async(weaveffi_cancel_token* cancel_token, weaveffi_edge_cases_async_list_callback callback, void* context); +WEAVEFFI_API void weaveffi_edge_cases_async_optional_async(weaveffi_edge_cases_async_optional_callback callback, void* context); +WEAVEFFI_API void weaveffi_edge_cases_async_enum_async(weaveffi_edge_cases_async_enum_callback callback, void* context); +WEAVEFFI_API void weaveffi_edge_cases_async_keywords_async(const char* class_, int32_t import, weaveffi_edge_cases_async_keywords_callback callback, void* context); +WEAVEFFI_API const uint8_t* weaveffi_edge_cases_keywords_roundtrip(const uint8_t* value_ptr, size_t value_len, size_t* out_len, weaveffi_error* out_err); +WEAVEFFI_DEPRECATED("Use Mode-based APIs") +WEAVEFFI_API const uint8_t* weaveffi_edge_cases_legacy_value(weaveffi_edge_cases_Legacy value, size_t* out_len, weaveffi_error* out_err); + + +#ifdef __cplusplus +} +#endif + +#endif // WEAVEFFI_H + +// END weaveffi.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_events__weaveffi_c.snap b/crates/weaveffi-cli/tests/snapshots/c_events__weaveffi_c.snap deleted file mode 100644 index d88de6a6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_events__weaveffi_c.snap +++ /dev/null @@ -1,27 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include "weaveffi.h" - -#include - -// Default implementations of the linear-memory allocator declared in -// weaveffi.h. The Wasm JS glue stages call arguments through these; a -// producer that ships its own allocator can omit this file from the -// build. The size is unused on dealloc because free() recovers it. -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size) { - return (uint8_t*)malloc(size ? size : 1u); -} - -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size) { - (void)size; - free(ptr); -} - -// Optional convenience wrappers can be added here in future versions. - -// END weaveffi.c diff --git a/crates/weaveffi-cli/tests/snapshots/c_events__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_events__weaveffi_h.snap deleted file mode 100644 index a3c299b9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_events__weaveffi_h.snap +++ /dev/null @@ -1,112 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#ifndef WEAVEFFI_H -#define WEAVEFFI_H - -#include -#include -#include - -#ifndef WEAVEFFI_API -# if defined(_WIN32) || defined(__CYGWIN__) -# ifdef WEAVEFFI_BUILD -# define WEAVEFFI_API __declspec(dllexport) -# else -# define WEAVEFFI_API __declspec(dllimport) -# endif -# elif defined(__EMSCRIPTEN__) -# define WEAVEFFI_API __attribute__((used, visibility("default"))) -# elif defined(__GNUC__) && (__GNUC__ >= 4) -# define WEAVEFFI_API __attribute__((visibility("default"))) -# else -# define WEAVEFFI_API -# endif -#endif - -#ifndef WEAVEFFI_DEPRECATED -# if defined(_MSC_VER) -# define WEAVEFFI_DEPRECATED(msg) __declspec(deprecated(msg)) -# elif defined(__GNUC__) || defined(__clang__) -# define WEAVEFFI_DEPRECATED(msg) __attribute__((deprecated(msg))) -# else -# define WEAVEFFI_DEPRECATED(msg) -# endif -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -typedef uint64_t weaveffi_handle_t; - -/* Error slot written by every fallible call. `payload_ptr`/`payload_len` - hold the matched error code's fields serialized in the WeaveFFI value - buffer format (null when the code declares no fields); both the message - and the payload are released by weaveffi_error_clear. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); - -/* Async completion callbacks receive a heap-boxed error the consumer - owns; weaveffi_error_free releases the message, the payload, and the - box itself. Passing NULL is a safe no-op. */ -WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); -WEAVEFFI_API void weaveffi_free_string(const char* ptr); -WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); - -/* Linear-memory allocator used by the Wasm JS glue to stage call - arguments. Native consumers never call these; producers targeting - WebAssembly must export them (the generated weaveffi.c provides - malloc/free-backed defaults). */ -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size); -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size); - -typedef struct weaveffi_cancel_token weaveffi_cancel_token; -WEAVEFFI_API weaveffi_cancel_token* weaveffi_cancel_token_create(void); -WEAVEFFI_API void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); -WEAVEFFI_API bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); -WEAVEFFI_API void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); - -/* - * Value buffer convention: records, rich enums, lists, maps, and - * optionals cross the ABI serialized in the WeaveFFI value buffer - * format (little-endian; see the generated bindings or the WeaveFFI - * buffer protocol spec for the per-type encoding). - * A buffered parameter named "v" expands to a borrowed view: - * const uint8_t* v_ptr, size_t v_len - * A buffered return is a producer-allocated buffer returned as - * `const uint8_t*` with a trailing `size_t* out_len`; the caller - * decodes it and then releases it with weaveffi_free_bytes. - */ - -typedef struct weaveffi_events_GetMessagesIterator weaveffi_events_GetMessagesIterator; -typedef void (*weaveffi_events_OnMessage_fn)(const char* message, void* context); - -// Module: events -WEAVEFFI_API uint64_t weaveffi_events_register_message_listener(weaveffi_events_OnMessage_fn callback, void* context); -WEAVEFFI_API void weaveffi_events_unregister_message_listener(uint64_t id); -/** Send a message, triggering the OnMessage callback */ -WEAVEFFI_API void weaveffi_events_send_message(const char* text, weaveffi_error* out_err); -/** Return an iterator over all sent messages */ -WEAVEFFI_API weaveffi_events_GetMessagesIterator* weaveffi_events_get_messages(weaveffi_error* out_err); -WEAVEFFI_API int32_t weaveffi_events_GetMessagesIterator_next(weaveffi_events_GetMessagesIterator* iter, const char** out_item, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_events_GetMessagesIterator_destroy(weaveffi_events_GetMessagesIterator* iter); - - -#ifdef __cplusplus -} -#endif - -#endif // WEAVEFFI_H - -// END weaveffi.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_inventory__weaveffi_c.snap b/crates/weaveffi-cli/tests/snapshots/c_inventory__weaveffi_c.snap deleted file mode 100644 index d88de6a6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_inventory__weaveffi_c.snap +++ /dev/null @@ -1,27 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include "weaveffi.h" - -#include - -// Default implementations of the linear-memory allocator declared in -// weaveffi.h. The Wasm JS glue stages call arguments through these; a -// producer that ships its own allocator can omit this file from the -// build. The size is unused on dealloc because free() recovers it. -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size) { - return (uint8_t*)malloc(size ? size : 1u); -} - -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size) { - (void)size; - free(ptr); -} - -// Optional convenience wrappers can be added here in future versions. - -// END weaveffi.c diff --git a/crates/weaveffi-cli/tests/snapshots/c_inventory__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_inventory__weaveffi_h.snap deleted file mode 100644 index f76a89d5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_inventory__weaveffi_h.snap +++ /dev/null @@ -1,123 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#ifndef WEAVEFFI_H -#define WEAVEFFI_H - -#include -#include -#include - -#ifndef WEAVEFFI_API -# if defined(_WIN32) || defined(__CYGWIN__) -# ifdef WEAVEFFI_BUILD -# define WEAVEFFI_API __declspec(dllexport) -# else -# define WEAVEFFI_API __declspec(dllimport) -# endif -# elif defined(__EMSCRIPTEN__) -# define WEAVEFFI_API __attribute__((used, visibility("default"))) -# elif defined(__GNUC__) && (__GNUC__ >= 4) -# define WEAVEFFI_API __attribute__((visibility("default"))) -# else -# define WEAVEFFI_API -# endif -#endif - -#ifndef WEAVEFFI_DEPRECATED -# if defined(_MSC_VER) -# define WEAVEFFI_DEPRECATED(msg) __declspec(deprecated(msg)) -# elif defined(__GNUC__) || defined(__clang__) -# define WEAVEFFI_DEPRECATED(msg) __attribute__((deprecated(msg))) -# else -# define WEAVEFFI_DEPRECATED(msg) -# endif -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -typedef uint64_t weaveffi_handle_t; - -/* Error slot written by every fallible call. `payload_ptr`/`payload_len` - hold the matched error code's fields serialized in the WeaveFFI value - buffer format (null when the code declares no fields); both the message - and the payload are released by weaveffi_error_clear. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); - -/* Async completion callbacks receive a heap-boxed error the consumer - owns; weaveffi_error_free releases the message, the payload, and the - box itself. Passing NULL is a safe no-op. */ -WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); -WEAVEFFI_API void weaveffi_free_string(const char* ptr); -WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); - -/* Linear-memory allocator used by the Wasm JS glue to stage call - arguments. Native consumers never call these; producers targeting - WebAssembly must export them (the generated weaveffi.c provides - malloc/free-backed defaults). */ -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size); -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size); - -typedef struct weaveffi_cancel_token weaveffi_cancel_token; -WEAVEFFI_API weaveffi_cancel_token* weaveffi_cancel_token_create(void); -WEAVEFFI_API void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); -WEAVEFFI_API bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); -WEAVEFFI_API void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); - -/* - * Value buffer convention: records, rich enums, lists, maps, and - * optionals cross the ABI serialized in the WeaveFFI value buffer - * format (little-endian; see the generated bindings or the WeaveFFI - * buffer protocol spec for the per-type encoding). - * A buffered parameter named "v" expands to a borrowed view: - * const uint8_t* v_ptr, size_t v_len - * A buffered return is a producer-allocated buffer returned as - * `const uint8_t*` with a trailing `size_t* out_len`; the caller - * decodes it and then releases it with weaveffi_free_bytes. - */ - -typedef enum { weaveffi_products_Category_Electronics = 0, weaveffi_products_Category_Clothing = 1, weaveffi_products_Category_Food = 2, weaveffi_products_Category_Books = 3 } weaveffi_products_Category; -/** Error codes reported by throwing functions in the `products` module tree. */ -typedef enum { weaveffi_products_ProductsError_InvalidPrice = 1, weaveffi_products_ProductsError_ProductNotFound = 2 } weaveffi_products_ProductsError; -/** Error codes reported by throwing functions in the `orders` module tree. */ -typedef enum { weaveffi_orders_OrdersError_OrderNotFound = 1, weaveffi_orders_OrdersError_EmptyOrder = 2 } weaveffi_orders_OrdersError; -typedef struct weaveffi_products_Catalog weaveffi_products_Catalog; - -// Module: products -/** A product catalog owning its products */ -WEAVEFFI_API weaveffi_products_Catalog* weaveffi_products_Catalog_new(weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_products_Catalog_add_product(const weaveffi_products_Catalog* self, const char* name, double price, weaveffi_products_Category category, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_products_Catalog_get_product(const weaveffi_products_Catalog* self, int64_t id, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_products_Catalog_search(const weaveffi_products_Catalog* self, weaveffi_products_Category category, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API bool weaveffi_products_Catalog_update_price(const weaveffi_products_Catalog* self, int64_t id, double price, weaveffi_error* out_err); -WEAVEFFI_API bool weaveffi_products_Catalog_remove(const weaveffi_products_Catalog* self, int64_t id, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_products_Catalog_destroy(weaveffi_products_Catalog* self); - - -// Module: orders -WEAVEFFI_API int64_t weaveffi_orders_create_order(const uint8_t* items_ptr, size_t items_len, weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_orders_get_order(int64_t id, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API bool weaveffi_orders_cancel_order(int64_t id, weaveffi_error* out_err); -WEAVEFFI_API bool weaveffi_orders_add_product_to_order(int64_t order_id, const uint8_t* product_ptr, size_t product_len, weaveffi_error* out_err); - - -#ifdef __cplusplus -} -#endif - -#endif // WEAVEFFI_H - -// END weaveffi.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__weaveffi_h.snap index 6d9914ac..1e1598b9 100644 --- a/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__weaveffi_h.snap +++ b/crates/weaveffi-cli/tests/snapshots/c_kitchen_sink__weaveffi_h.snap @@ -42,6 +42,13 @@ source: crates/weaveffi-cli/tests/snapshots.rs extern "C" { #endif +/* The WeaveFFI C ABI revision this header was generated against. The + producer exports weaveffi_abi_version() so a consumer can refuse to load + a library built for a different revision instead of misreading its + error struct or value buffers. */ +#define WEAVEFFI_ABI_VERSION 1u +WEAVEFFI_API uint32_t weaveffi_abi_version(void); + typedef uint64_t weaveffi_handle_t; /* Error slot written by every fallible call. `payload_ptr`/`payload_len` diff --git a/crates/weaveffi-cli/tests/snapshots/c_kvstore__weaveffi_c.snap b/crates/weaveffi-cli/tests/snapshots/c_kvstore__weaveffi_c.snap deleted file mode 100644 index d88de6a6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_kvstore__weaveffi_c.snap +++ /dev/null @@ -1,27 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include "weaveffi.h" - -#include - -// Default implementations of the linear-memory allocator declared in -// weaveffi.h. The Wasm JS glue stages call arguments through these; a -// producer that ships its own allocator can omit this file from the -// build. The size is unused on dealloc because free() recovers it. -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size) { - return (uint8_t*)malloc(size ? size : 1u); -} - -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size) { - (void)size; - free(ptr); -} - -// Optional convenience wrappers can be added here in future versions. - -// END weaveffi.c diff --git a/crates/weaveffi-cli/tests/snapshots/c_kvstore__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_kvstore__weaveffi_h.snap deleted file mode 100644 index dd8a66de..00000000 --- a/crates/weaveffi-cli/tests/snapshots/c_kvstore__weaveffi_h.snap +++ /dev/null @@ -1,161 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#ifndef WEAVEFFI_H -#define WEAVEFFI_H - -#include -#include -#include - -#ifndef WEAVEFFI_API -# if defined(_WIN32) || defined(__CYGWIN__) -# ifdef WEAVEFFI_BUILD -# define WEAVEFFI_API __declspec(dllexport) -# else -# define WEAVEFFI_API __declspec(dllimport) -# endif -# elif defined(__EMSCRIPTEN__) -# define WEAVEFFI_API __attribute__((used, visibility("default"))) -# elif defined(__GNUC__) && (__GNUC__ >= 4) -# define WEAVEFFI_API __attribute__((visibility("default"))) -# else -# define WEAVEFFI_API -# endif -#endif - -#ifndef WEAVEFFI_DEPRECATED -# if defined(_MSC_VER) -# define WEAVEFFI_DEPRECATED(msg) __declspec(deprecated(msg)) -# elif defined(__GNUC__) || defined(__clang__) -# define WEAVEFFI_DEPRECATED(msg) __attribute__((deprecated(msg))) -# else -# define WEAVEFFI_DEPRECATED(msg) -# endif -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -typedef uint64_t weaveffi_handle_t; - -/* Error slot written by every fallible call. `payload_ptr`/`payload_len` - hold the matched error code's fields serialized in the WeaveFFI value - buffer format (null when the code declares no fields); both the message - and the payload are released by weaveffi_error_clear. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); - -/* Async completion callbacks receive a heap-boxed error the consumer - owns; weaveffi_error_free releases the message, the payload, and the - box itself. Passing NULL is a safe no-op. */ -WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); -WEAVEFFI_API void weaveffi_free_string(const char* ptr); -WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); - -/* Linear-memory allocator used by the Wasm JS glue to stage call - arguments. Native consumers never call these; producers targeting - WebAssembly must export them (the generated weaveffi.c provides - malloc/free-backed defaults). */ -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size); -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size); - -typedef struct weaveffi_cancel_token weaveffi_cancel_token; -WEAVEFFI_API weaveffi_cancel_token* weaveffi_cancel_token_create(void); -WEAVEFFI_API void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); -WEAVEFFI_API bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); -WEAVEFFI_API void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); - -/* - * Value buffer convention: records, rich enums, lists, maps, and - * optionals cross the ABI serialized in the WeaveFFI value buffer - * format (little-endian; see the generated bindings or the WeaveFFI - * buffer protocol spec for the per-type encoding). - * A buffered parameter named "v" expands to a borrowed view: - * const uint8_t* v_ptr, size_t v_len - * A buffered return is a producer-allocated buffer returned as - * `const uint8_t*` with a trailing `size_t* out_len`; the caller - * decodes it and then releases it with weaveffi_free_bytes. - */ - -/** Persistence semantics applied to a stored entry */ -typedef enum { - /** In-memory only; lost on close */ - weaveffi_kv_EntryKind_Volatile = 0, - /** Flushed to durable storage */ - weaveffi_kv_EntryKind_Persistent = 1, - /** Persistent and encrypted at rest */ - weaveffi_kv_EntryKind_Encrypted = 2 -} weaveffi_kv_EntryKind; -/** Error codes reported by throwing functions in the `kv` module tree. */ -typedef enum { - /** No entry exists for the requested key */ - weaveffi_kv_KvError_KeyNotFound = 1001, - /** The entry exists but its TTL has elapsed */ - weaveffi_kv_KvError_Expired = 1002, - /** The store has reached its capacity limit */ - weaveffi_kv_KvError_StoreFull = 1003, - /** Underlying storage reported an I/O failure */ - weaveffi_kv_KvError_IoError = 1004 -} weaveffi_kv_KvError; -typedef struct weaveffi_kv_Store weaveffi_kv_Store; -typedef struct weaveffi_kv_Store_ListKeysIterator weaveffi_kv_Store_ListKeysIterator; -/** Fires when an entry is evicted from the store */ -typedef void (*weaveffi_kv_OnEvict_fn)(const char* key, void* context); -typedef void (*weaveffi_kv_Store_compact_callback)(void* context, weaveffi_error* err, int64_t result); - -// Module: kv -/** An in-memory key-value store owning its entries; destroying the object releases all of its state */ -/** Open (or create) a store backed by the given filesystem path */ -WEAVEFFI_API weaveffi_kv_Store* weaveffi_kv_Store_open(const char* path, weaveffi_error* out_err); -/** The largest number of live entries one store will hold */ -WEAVEFFI_API int64_t weaveffi_kv_Store_default_capacity(weaveffi_error* out_err); -/** Insert or replace a value, returning true on success */ -WEAVEFFI_API bool weaveffi_kv_Store_put(const weaveffi_kv_Store* self, const char* key, const uint8_t* value_ptr, size_t value_len, weaveffi_kv_EntryKind kind, const uint8_t* ttl_seconds_ptr, size_t ttl_seconds_len, weaveffi_error* out_err); -/** Look up an entry by key; returns null if missing or expired */ -WEAVEFFI_API const uint8_t* weaveffi_kv_Store_get(const weaveffi_kv_Store* self, const char* key, size_t* out_len, weaveffi_error* out_err); -/** Remove the entry for the given key, returning true if it existed */ -WEAVEFFI_API bool weaveffi_kv_Store_delete(const weaveffi_kv_Store* self, const char* key, weaveffi_error* out_err); -/** Stream every key, optionally filtered by a prefix */ -WEAVEFFI_API weaveffi_kv_Store_ListKeysIterator* weaveffi_kv_Store_list_keys(const weaveffi_kv_Store* self, const uint8_t* prefix_ptr, size_t prefix_len, weaveffi_error* out_err); -WEAVEFFI_API int32_t weaveffi_kv_Store_ListKeysIterator_next(weaveffi_kv_Store_ListKeysIterator* iter, const char** out_item, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_kv_Store_ListKeysIterator_destroy(weaveffi_kv_Store_ListKeysIterator* iter); -/** Return the number of live entries in the store */ -WEAVEFFI_API int64_t weaveffi_kv_Store_count(const weaveffi_kv_Store* self, weaveffi_error* out_err); -/** Drop every entry from the store */ -WEAVEFFI_API void weaveffi_kv_Store_clear(const weaveffi_kv_Store* self, weaveffi_error* out_err); -/** Reclaim space asynchronously; returns the number of bytes reclaimed */ -WEAVEFFI_API void weaveffi_kv_Store_compact_async(const weaveffi_kv_Store* self, weaveffi_cancel_token* cancel_token, weaveffi_kv_Store_compact_callback callback, void* context); -/** Legacy single-shot put kept for compatibility */ -WEAVEFFI_DEPRECATED("use put() with explicit kind") -WEAVEFFI_API bool weaveffi_kv_Store_legacy_put(const weaveffi_kv_Store* self, const char* key, const uint8_t* value_ptr, size_t value_len, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_kv_Store_destroy(weaveffi_kv_Store* self); - -/** Subscribe to per-key eviction notifications */ -WEAVEFFI_API uint64_t weaveffi_kv_register_eviction_listener(weaveffi_kv_OnEvict_fn callback, void* context); -/** Subscribe to per-key eviction notifications */ -WEAVEFFI_API void weaveffi_kv_unregister_eviction_listener(uint64_t id); - -// Module: kv_stats -/** Snapshot the current store statistics */ -WEAVEFFI_API const uint8_t* weaveffi_kv_stats_get_stats(const weaveffi_kv_Store* store, size_t* out_len, weaveffi_error* out_err); - - -#ifdef __cplusplus -} -#endif - -#endif // WEAVEFFI_H - -// END weaveffi.h diff --git a/crates/weaveffi-cli/tests/snapshots/c_nested_modules__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_nested_modules__weaveffi_h.snap index f334f7ae..75a21d3f 100644 --- a/crates/weaveffi-cli/tests/snapshots/c_nested_modules__weaveffi_h.snap +++ b/crates/weaveffi-cli/tests/snapshots/c_nested_modules__weaveffi_h.snap @@ -42,6 +42,13 @@ source: crates/weaveffi-cli/tests/snapshots.rs extern "C" { #endif +/* The WeaveFFI C ABI revision this header was generated against. The + producer exports weaveffi_abi_version() so a consumer can refuse to load + a library built for a different revision instead of misreading its + error struct or value buffers. */ +#define WEAVEFFI_ABI_VERSION 1u +WEAVEFFI_API uint32_t weaveffi_abi_version(void); + typedef uint64_t weaveffi_handle_t; /* Error slot written by every fallible call. `payload_ptr`/`payload_len` diff --git a/crates/weaveffi-cli/tests/snapshots/c_shapes__weaveffi_h.snap b/crates/weaveffi-cli/tests/snapshots/c_shapes__weaveffi_h.snap index 51187592..c782b656 100644 --- a/crates/weaveffi-cli/tests/snapshots/c_shapes__weaveffi_h.snap +++ b/crates/weaveffi-cli/tests/snapshots/c_shapes__weaveffi_h.snap @@ -42,6 +42,13 @@ source: crates/weaveffi-cli/tests/snapshots.rs extern "C" { #endif +/* The WeaveFFI C ABI revision this header was generated against. The + producer exports weaveffi_abi_version() so a consumer can refuse to load + a library built for a different revision instead of misreading its + error struct or value buffers. */ +#define WEAVEFFI_ABI_VERSION 1u +WEAVEFFI_API uint32_t weaveffi_abi_version(void); + typedef uint64_t weaveffi_handle_t; /* Error slot written by every fallible call. `payload_ptr`/`payload_len` diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_async_demo__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_async_demo__weaveffi_hpp.snap deleted file mode 100644 index b8284f45..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_async_demo__weaveffi_hpp.snap +++ /dev/null @@ -1,454 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef WEAVEFFI_API -# if defined(_WIN32) || defined(__CYGWIN__) -# ifdef WEAVEFFI_BUILD -# define WEAVEFFI_API __declspec(dllexport) -# else -# define WEAVEFFI_API __declspec(dllimport) -# endif -# elif defined(__EMSCRIPTEN__) -# define WEAVEFFI_API __attribute__((used, visibility("default"))) -# elif defined(__GNUC__) && (__GNUC__ >= 4) -# define WEAVEFFI_API __attribute__((visibility("default"))) -# else -# define WEAVEFFI_API -# endif -#endif - -#ifndef WEAVEFFI_DEPRECATED -# if defined(_MSC_VER) -# define WEAVEFFI_DEPRECATED(msg) __declspec(deprecated(msg)) -# elif defined(__GNUC__) || defined(__clang__) -# define WEAVEFFI_DEPRECATED(msg) __attribute__((deprecated(msg))) -# else -# define WEAVEFFI_DEPRECATED(msg) -# endif -#endif - -extern "C" { - -typedef uint64_t weaveffi_handle_t; - -/* Error slot written by every fallible call. `payload_ptr`/`payload_len` - hold the matched error code's fields serialized in the WeaveFFI value - buffer format (null when the code declares no fields); both the message - and the payload are released by weaveffi_error_clear. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); - -/* Async completion callbacks receive a heap-boxed error the consumer - owns; weaveffi_error_free releases the message, the payload, and the - box itself. Passing NULL is a safe no-op. */ -WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); -WEAVEFFI_API void weaveffi_free_string(const char* ptr); -WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); - -/* Linear-memory allocator used by the Wasm JS glue to stage call - arguments. Native consumers never call these; producers targeting - WebAssembly must export them (the generated weaveffi.c provides - malloc/free-backed defaults). */ -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size); -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size); - -typedef struct weaveffi_cancel_token weaveffi_cancel_token; -WEAVEFFI_API weaveffi_cancel_token* weaveffi_cancel_token_create(void); -WEAVEFFI_API void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); -WEAVEFFI_API bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); -WEAVEFFI_API void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); - -/** Error codes reported by throwing functions in the `tasks` module tree. */ -typedef enum { weaveffi_tasks_TaskError_InvalidName = 1 } weaveffi_tasks_TaskError; -typedef void (*weaveffi_tasks_run_task_callback)(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len); -typedef void (*weaveffi_tasks_run_batch_callback)(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len); -typedef void (*weaveffi_tasks_run_n_tasks_callback)(void* context, weaveffi_error* err, int32_t result); - -WEAVEFFI_API void weaveffi_tasks_run_task_async(const char* name, weaveffi_tasks_run_task_callback callback, void* context); -WEAVEFFI_API void weaveffi_tasks_run_batch_async(const uint8_t* names_ptr, size_t names_len, weaveffi_tasks_run_batch_callback callback, void* context); -WEAVEFFI_API bool weaveffi_tasks_cancel_task(int64_t id, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_tasks_run_n_tasks_async(int32_t n, weaveffi_tasks_run_n_tasks_callback callback, void* context); -WEAVEFFI_API int64_t weaveffi_tasks_active_callbacks(weaveffi_error* out_err); - -} // extern "C" - -namespace weaveffi { - -/** Base exception for every error reported through the C ABI. */ -class WeaveFFIError : public std::runtime_error { - int32_t code_; - -public: - WeaveFFIError(int32_t code, const std::string& msg) : std::runtime_error(msg), code_(code) {} - int32_t code() const { return code_; } -}; - -namespace detail { - -/** Throw the generic WeaveFFIError if `err` carries a nonzero code. */ -inline void check(weaveffi_error& err) { - if (err.code == 0) return; - std::string msg(err.message ? err.message : "unknown error"); - int32_t code = err.code; - weaveffi_error_clear(&err); - throw WeaveFFIError(code, msg); -} - -/** Wrap an async-callback error as the generic WeaveFFIError. */ -inline std::exception_ptr make_error(int32_t code, const std::string& msg) { - return std::make_exception_ptr(WeaveFFIError(code, msg)); -} - -} // namespace detail - -namespace detail { - -/** - * Serializes values into the WeaveFFI 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 { return buf_.data(); } - - /** Number of encoded bytes. */ - size_t size() const { 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 string, byte-buffer, or collection length as a u32. */ - void write_len(size_t n) { append_le(static_cast(n)); } - - void write_string(const std::string& 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); } -}; - -/** - * Decodes values from the WeaveFFI value-buffer wire format. A malformed - * buffer is a producer/consumer contract violation (both sides are generated - * from one IDL), so every decode failure throws the generic WeaveFFIError, - * the same channel as a producer panic. - */ -class BufferReader { - const uint8_t* data_; - size_t len_; - size_t pos_; - - [[noreturn]] static void fail(const char* what) { - throw WeaveFFIError(-2, std::string("malformed WeaveFFI value buffer: ") + what); - } - - 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: - BufferReader(const uint8_t* data, size_t len) : data_(data), len_(len), pos_(0) {} - - /** Bytes not yet consumed. */ - size_t remaining() const { 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 length prefix, 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); - } - - 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; - } - - /** 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 weaveffi_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() { - if (ptr != nullptr) weaveffi_free_bytes(const_cast(ptr), len); - } -}; - -} // namespace detail - -struct TaskResult { - int64_t id; - std::string value; - bool success; -}; - -namespace detail { - -/** Encodes a `TaskResult` in the WeaveFFI value-buffer format. */ -inline void write_TaskResult(BufferWriter& w, const TaskResult& v) { - w.write_i64(v.id); - w.write_string(v.value); - w.write_bool(v.success); -} - -/** Decodes a `TaskResult` from the WeaveFFI value-buffer format. */ -inline TaskResult read_TaskResult(BufferReader& r) { - TaskResult out{}; - out.id = r.read_i64(); - out.value = r.read_string(); - out.success = r.read_bool(); - return out; -} - -} // namespace detail - -/** Typed errors reported by the `tasks` module's throwing functions. */ -class TaskError : public WeaveFFIError { -public: - TaskError(int32_t code, const std::string& msg) : WeaveFFIError(code, msg) {} -}; - -/** task name must not be empty */ -class InvalidNameError : public TaskError { -public: - InvalidNameError(const std::string& msg) : TaskError(1, msg) {} -}; - -namespace detail { - -/** Map a `tasks` error code and payload to its typed exception (WeaveFFIError for runtime codes, TaskError for unknown codes). */ -inline std::exception_ptr make_tasks_error(int32_t code, const std::string& msg, const uint8_t* payload_ptr, size_t payload_len) { - if (code < 0) return std::make_exception_ptr(WeaveFFIError(code, msg)); - switch (code) { - case 1: return std::make_exception_ptr(InvalidNameError(msg)); - default: return std::make_exception_ptr(TaskError(code, msg)); - } -} - -/** Throw the typed `tasks` exception if `err` carries a nonzero code. */ -inline void check_tasks(weaveffi_error& err) { - if (err.code == 0) return; - std::string msg(err.message ? err.message : "unknown error"); - std::exception_ptr ex = make_tasks_error(err.code, msg, err.payload_ptr, err.payload_len); - weaveffi_error_clear(&err); - std::rethrow_exception(ex); -} - -} // namespace detail - -namespace tasks { - -inline std::future run_task(const std::string& name) { - auto* promise_ptr = new std::promise(); - auto future = promise_ptr->get_future(); - weaveffi_tasks_run_task_async(name.c_str(), [](void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { - auto* p = static_cast*>(context); - if (err && err->code != 0) { - std::string msg(err->message ? err->message : "unknown error"); - p->set_exception(detail::make_tasks_error(err->code, msg, err->payload_ptr, err->payload_len)); - weaveffi_error_free(err); - } else { - detail::BufferReader result_r(result_ptr, result_len); - TaskResult value = detail::read_TaskResult(result_r); - result_r.expect_end(); - weaveffi_free_bytes(const_cast(result_ptr), result_len); - p->set_value(std::move(value)); - } - delete p; - }, static_cast(promise_ptr)); - return future; -} - -inline std::future> run_batch(const std::vector& names) { - auto* promise_ptr = new std::promise>(); - auto future = promise_ptr->get_future(); - detail::BufferWriter names_buf; - names_buf.write_len(names.size()); - for (const auto& item0 : names) { - names_buf.write_string(item0); - } - weaveffi_tasks_run_batch_async(names_buf.data(), names_buf.size(), [](void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { - auto* p = static_cast>*>(context); - if (err && err->code != 0) { - std::string msg(err->message ? err->message : "unknown error"); - p->set_exception(detail::make_error(err->code, msg)); - weaveffi_error_free(err); - } else { - detail::BufferReader result_r(result_ptr, result_len); - std::vector value{}; - { - size_t value_n = result_r.read_len(); - value.reserve(value_n); - for (size_t value_i = 0; value_i < value_n; ++value_i) { - TaskResult value_item = detail::read_TaskResult(result_r); - value.push_back(std::move(value_item)); - } - } - result_r.expect_end(); - weaveffi_free_bytes(const_cast(result_ptr), result_len); - p->set_value(std::move(value)); - } - delete p; - }, static_cast(promise_ptr)); - return future; -} - -inline bool cancel_task(int64_t id) { - weaveffi_error err{}; - auto result = weaveffi_tasks_cancel_task(id, &err); - detail::check(err); - return result; -} - -inline std::future run_n_tasks(int32_t n) { - auto* promise_ptr = new std::promise(); - auto future = promise_ptr->get_future(); - weaveffi_tasks_run_n_tasks_async(n, [](void* context, weaveffi_error* err, int32_t result) { - auto* p = static_cast*>(context); - if (err && err->code != 0) { - std::string msg(err->message ? err->message : "unknown error"); - p->set_exception(detail::make_error(err->code, msg)); - weaveffi_error_free(err); - } else { - p->set_value(result); - } - delete p; - }, static_cast(promise_ptr)); - return future; -} - -inline int64_t active_callbacks() { - weaveffi_error err{}; - auto result = weaveffi_tasks_active_callbacks(&err); - detail::check(err); - return result; -} - -} // namespace tasks - -} // namespace weaveffi - -// END weaveffi.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_calculator__CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/cpp_calculator__CMakeLists_txt.snap deleted file mode 100644 index f169a251..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_calculator__CMakeLists_txt.snap +++ /dev/null @@ -1,15 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -cmake_minimum_required(VERSION 3.14) -project(weaveffi_cpp VERSION 0.1.0) -add_library(weaveffi_cpp INTERFACE) -target_include_directories(weaveffi_cpp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(weaveffi_cpp INTERFACE weaveffi) -target_compile_features(weaveffi_cpp INTERFACE cxx_std_17) - -# END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_calculator__README_md.snap b/crates/weaveffi-cli/tests/snapshots/cpp_calculator__README_md.snap deleted file mode 100644 index da3ea6c6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_calculator__README_md.snap +++ /dev/null @@ -1,39 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI C++ Bindings - -## Prerequisites - -- CMake 3.14+ -- C++17 compiler -- The `weaveffi` static/shared library built from the Rust crate - -## Usage with CMake - -Add the generated `cpp/` directory as a subdirectory in your `CMakeLists.txt` and -link against `weaveffi_cpp`: - -```cmake -add_subdirectory(path/to/generated/cpp) -add_executable(myapp main.cpp) -target_link_libraries(myapp weaveffi_cpp) -``` - -The `weaveffi_cpp` target is an INTERFACE library that: - -- Adds the generated header directory to your include path -- Links against the `weaveffi` library -- Requires C++17 - -Then include the header in your code: - -```cpp -#include "weaveffi.hpp" -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_calculator__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_calculator__weaveffi_hpp.snap deleted file mode 100644 index 7ebc0ca7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_calculator__weaveffi_hpp.snap +++ /dev/null @@ -1,199 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef WEAVEFFI_API -# if defined(_WIN32) || defined(__CYGWIN__) -# ifdef WEAVEFFI_BUILD -# define WEAVEFFI_API __declspec(dllexport) -# else -# define WEAVEFFI_API __declspec(dllimport) -# endif -# elif defined(__EMSCRIPTEN__) -# define WEAVEFFI_API __attribute__((used, visibility("default"))) -# elif defined(__GNUC__) && (__GNUC__ >= 4) -# define WEAVEFFI_API __attribute__((visibility("default"))) -# else -# define WEAVEFFI_API -# endif -#endif - -#ifndef WEAVEFFI_DEPRECATED -# if defined(_MSC_VER) -# define WEAVEFFI_DEPRECATED(msg) __declspec(deprecated(msg)) -# elif defined(__GNUC__) || defined(__clang__) -# define WEAVEFFI_DEPRECATED(msg) __attribute__((deprecated(msg))) -# else -# define WEAVEFFI_DEPRECATED(msg) -# endif -#endif - -extern "C" { - -typedef uint64_t weaveffi_handle_t; - -/* Error slot written by every fallible call. `payload_ptr`/`payload_len` - hold the matched error code's fields serialized in the WeaveFFI value - buffer format (null when the code declares no fields); both the message - and the payload are released by weaveffi_error_clear. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); - -/* Async completion callbacks receive a heap-boxed error the consumer - owns; weaveffi_error_free releases the message, the payload, and the - box itself. Passing NULL is a safe no-op. */ -WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); -WEAVEFFI_API void weaveffi_free_string(const char* ptr); -WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); - -/* Linear-memory allocator used by the Wasm JS glue to stage call - arguments. Native consumers never call these; producers targeting - WebAssembly must export them (the generated weaveffi.c provides - malloc/free-backed defaults). */ -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size); -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size); - -typedef struct weaveffi_cancel_token weaveffi_cancel_token; -WEAVEFFI_API weaveffi_cancel_token* weaveffi_cancel_token_create(void); -WEAVEFFI_API void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); -WEAVEFFI_API bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); -WEAVEFFI_API void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); - -/** Error codes reported by throwing functions in the `calculator` module tree. */ -typedef enum { weaveffi_calculator_CalcError_DivisionByZero = 1 } weaveffi_calculator_CalcError; - -/** Add two integers */ -WEAVEFFI_API int32_t weaveffi_calculator_add(int32_t a, int32_t b, weaveffi_error* out_err); -/** Multiply two integers */ -WEAVEFFI_API int32_t weaveffi_calculator_mul(int32_t a, int32_t b, weaveffi_error* out_err); -/** Divide two integers, failing on a zero divisor */ -WEAVEFFI_API int32_t weaveffi_calculator_div(int32_t a, int32_t b, weaveffi_error* out_err); -/** Echo a string back */ -WEAVEFFI_API const char* weaveffi_calculator_echo(const char* s, weaveffi_error* out_err); - -} // extern "C" - -namespace weaveffi { - -/** Base exception for every error reported through the C ABI. */ -class WeaveFFIError : public std::runtime_error { - int32_t code_; - -public: - WeaveFFIError(int32_t code, const std::string& msg) : std::runtime_error(msg), code_(code) {} - int32_t code() const { return code_; } -}; - -namespace detail { - -/** Throw the generic WeaveFFIError if `err` carries a nonzero code. */ -inline void check(weaveffi_error& err) { - if (err.code == 0) return; - std::string msg(err.message ? err.message : "unknown error"); - int32_t code = err.code; - weaveffi_error_clear(&err); - throw WeaveFFIError(code, msg); -} - -/** Wrap an async-callback error as the generic WeaveFFIError. */ -inline std::exception_ptr make_error(int32_t code, const std::string& msg) { - return std::make_exception_ptr(WeaveFFIError(code, msg)); -} - -} // namespace detail - -/** Typed errors reported by the `calculator` module's throwing functions. */ -class CalcError : public WeaveFFIError { -public: - CalcError(int32_t code, const std::string& msg) : WeaveFFIError(code, msg) {} -}; - -/** division by zero */ -class DivisionByZeroError : public CalcError { -public: - DivisionByZeroError(const std::string& msg) : CalcError(1, msg) {} -}; - -namespace detail { - -/** Map a `calculator` error code and payload to its typed exception (WeaveFFIError for runtime codes, CalcError for unknown codes). */ -inline std::exception_ptr make_calculator_error(int32_t code, const std::string& msg, const uint8_t* payload_ptr, size_t payload_len) { - if (code < 0) return std::make_exception_ptr(WeaveFFIError(code, msg)); - switch (code) { - case 1: return std::make_exception_ptr(DivisionByZeroError(msg)); - default: return std::make_exception_ptr(CalcError(code, msg)); - } -} - -/** Throw the typed `calculator` exception if `err` carries a nonzero code. */ -inline void check_calculator(weaveffi_error& err) { - if (err.code == 0) return; - std::string msg(err.message ? err.message : "unknown error"); - std::exception_ptr ex = make_calculator_error(err.code, msg, err.payload_ptr, err.payload_len); - weaveffi_error_clear(&err); - std::rethrow_exception(ex); -} - -} // namespace detail - -namespace calculator { - -/** Add two integers */ -inline int32_t add(int32_t a, int32_t b) { - weaveffi_error err{}; - auto result = weaveffi_calculator_add(a, b, &err); - detail::check(err); - return result; -} - -/** Multiply two integers */ -inline int32_t mul(int32_t a, int32_t b) { - weaveffi_error err{}; - auto result = weaveffi_calculator_mul(a, b, &err); - detail::check(err); - return result; -} - -/** Divide two integers, failing on a zero divisor */ -inline int32_t div(int32_t a, int32_t b) { - weaveffi_error err{}; - auto result = weaveffi_calculator_div(a, b, &err); - detail::check_calculator(err); - return result; -} - -/** Echo a string back */ -inline std::string echo(const std::string& s) { - weaveffi_error err{}; - auto result = weaveffi_calculator_echo(s.c_str(), &err); - detail::check(err); - std::string ret(result); - weaveffi_free_string(result); - return ret; -} - -} // namespace calculator - -} // namespace weaveffi - -// END weaveffi.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_contacts__CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/cpp_contacts__CMakeLists_txt.snap deleted file mode 100644 index f169a251..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_contacts__CMakeLists_txt.snap +++ /dev/null @@ -1,15 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -cmake_minimum_required(VERSION 3.14) -project(weaveffi_cpp VERSION 0.1.0) -add_library(weaveffi_cpp INTERFACE) -target_include_directories(weaveffi_cpp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(weaveffi_cpp INTERFACE weaveffi) -target_compile_features(weaveffi_cpp INTERFACE cxx_std_17) - -# END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_contacts__README_md.snap b/crates/weaveffi-cli/tests/snapshots/cpp_contacts__README_md.snap deleted file mode 100644 index da3ea6c6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_contacts__README_md.snap +++ /dev/null @@ -1,39 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI C++ Bindings - -## Prerequisites - -- CMake 3.14+ -- C++17 compiler -- The `weaveffi` static/shared library built from the Rust crate - -## Usage with CMake - -Add the generated `cpp/` directory as a subdirectory in your `CMakeLists.txt` and -link against `weaveffi_cpp`: - -```cmake -add_subdirectory(path/to/generated/cpp) -add_executable(myapp main.cpp) -target_link_libraries(myapp weaveffi_cpp) -``` - -The `weaveffi_cpp` target is an INTERFACE library that: - -- Adds the generated header directory to your include path -- Links against the `weaveffi` library -- Requires C++17 - -Then include the header in your code: - -```cpp -#include "weaveffi.hpp" -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_contacts__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_contacts__weaveffi_hpp.snap deleted file mode 100644 index eb4799ee..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_contacts__weaveffi_hpp.snap +++ /dev/null @@ -1,492 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef WEAVEFFI_API -# if defined(_WIN32) || defined(__CYGWIN__) -# ifdef WEAVEFFI_BUILD -# define WEAVEFFI_API __declspec(dllexport) -# else -# define WEAVEFFI_API __declspec(dllimport) -# endif -# elif defined(__EMSCRIPTEN__) -# define WEAVEFFI_API __attribute__((used, visibility("default"))) -# elif defined(__GNUC__) && (__GNUC__ >= 4) -# define WEAVEFFI_API __attribute__((visibility("default"))) -# else -# define WEAVEFFI_API -# endif -#endif - -#ifndef WEAVEFFI_DEPRECATED -# if defined(_MSC_VER) -# define WEAVEFFI_DEPRECATED(msg) __declspec(deprecated(msg)) -# elif defined(__GNUC__) || defined(__clang__) -# define WEAVEFFI_DEPRECATED(msg) __attribute__((deprecated(msg))) -# else -# define WEAVEFFI_DEPRECATED(msg) -# endif -#endif - -extern "C" { - -typedef uint64_t weaveffi_handle_t; - -/* Error slot written by every fallible call. `payload_ptr`/`payload_len` - hold the matched error code's fields serialized in the WeaveFFI value - buffer format (null when the code declares no fields); both the message - and the payload are released by weaveffi_error_clear. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); - -/* Async completion callbacks receive a heap-boxed error the consumer - owns; weaveffi_error_free releases the message, the payload, and the - box itself. Passing NULL is a safe no-op. */ -WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); -WEAVEFFI_API void weaveffi_free_string(const char* ptr); -WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); - -/* Linear-memory allocator used by the Wasm JS glue to stage call - arguments. Native consumers never call these; producers targeting - WebAssembly must export them (the generated weaveffi.c provides - malloc/free-backed defaults). */ -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size); -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size); - -typedef struct weaveffi_cancel_token weaveffi_cancel_token; -WEAVEFFI_API weaveffi_cancel_token* weaveffi_cancel_token_create(void); -WEAVEFFI_API void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); -WEAVEFFI_API bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); -WEAVEFFI_API void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); - -typedef enum { weaveffi_contacts_ContactType_Personal = 0, weaveffi_contacts_ContactType_Work = 1, weaveffi_contacts_ContactType_Other = 2 } weaveffi_contacts_ContactType; -/** Error codes reported by throwing functions in the `contacts` module tree. */ -typedef enum { weaveffi_contacts_ContactsError_InvalidName = 1, weaveffi_contacts_ContactsError_NotFound = 2 } weaveffi_contacts_ContactsError; -typedef struct weaveffi_contacts_ContactBook weaveffi_contacts_ContactBook; - -/** An in-memory address book owning its contacts */ -WEAVEFFI_API weaveffi_contacts_ContactBook* weaveffi_contacts_ContactBook_new(weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_contacts_ContactBook_add(const weaveffi_contacts_ContactBook* self, const char* first_name, const char* last_name, const uint8_t* email_ptr, size_t email_len, weaveffi_contacts_ContactType contact_type, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_contacts_ContactBook_get(const weaveffi_contacts_ContactBook* self, int64_t id, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_contacts_ContactBook_list(const weaveffi_contacts_ContactBook* self, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API bool weaveffi_contacts_ContactBook_remove(const weaveffi_contacts_ContactBook* self, int64_t id, weaveffi_error* out_err); -WEAVEFFI_API int32_t weaveffi_contacts_ContactBook_count(const weaveffi_contacts_ContactBook* self, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_contacts_ContactBook_destroy(weaveffi_contacts_ContactBook* self); - - -} // extern "C" - -namespace weaveffi { - -/** Base exception for every error reported through the C ABI. */ -class WeaveFFIError : public std::runtime_error { - int32_t code_; - -public: - WeaveFFIError(int32_t code, const std::string& msg) : std::runtime_error(msg), code_(code) {} - int32_t code() const { return code_; } -}; - -namespace detail { - -/** Throw the generic WeaveFFIError if `err` carries a nonzero code. */ -inline void check(weaveffi_error& err) { - if (err.code == 0) return; - std::string msg(err.message ? err.message : "unknown error"); - int32_t code = err.code; - weaveffi_error_clear(&err); - throw WeaveFFIError(code, msg); -} - -/** Wrap an async-callback error as the generic WeaveFFIError. */ -inline std::exception_ptr make_error(int32_t code, const std::string& msg) { - return std::make_exception_ptr(WeaveFFIError(code, msg)); -} - -} // namespace detail - -namespace detail { - -/** - * Serializes values into the WeaveFFI 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 { return buf_.data(); } - - /** Number of encoded bytes. */ - size_t size() const { 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 string, byte-buffer, or collection length as a u32. */ - void write_len(size_t n) { append_le(static_cast(n)); } - - void write_string(const std::string& 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); } -}; - -/** - * Decodes values from the WeaveFFI value-buffer wire format. A malformed - * buffer is a producer/consumer contract violation (both sides are generated - * from one IDL), so every decode failure throws the generic WeaveFFIError, - * the same channel as a producer panic. - */ -class BufferReader { - const uint8_t* data_; - size_t len_; - size_t pos_; - - [[noreturn]] static void fail(const char* what) { - throw WeaveFFIError(-2, std::string("malformed WeaveFFI value buffer: ") + what); - } - - 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: - BufferReader(const uint8_t* data, size_t len) : data_(data), len_(len), pos_(0) {} - - /** Bytes not yet consumed. */ - size_t remaining() const { 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 length prefix, 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); - } - - 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; - } - - /** 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 weaveffi_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() { - if (ptr != nullptr) weaveffi_free_bytes(const_cast(ptr), len); - } -}; - -} // namespace detail - -enum class ContactType : int32_t { - Personal = 0, - Work = 1, - Other = 2 -}; - -struct Contact { - int64_t id; - std::string first_name; - std::string last_name; - std::optional email; - ContactType contact_type; -}; - -namespace detail { - -/** Encodes a `Contact` in the WeaveFFI value-buffer format. */ -inline void write_Contact(BufferWriter& w, const Contact& v) { - w.write_i64(v.id); - w.write_string(v.first_name); - w.write_string(v.last_name); - w.write_option_flag(v.email.has_value()); - if (v.email.has_value()) { - w.write_string((*v.email)); - } - w.write_i32(static_cast(v.contact_type)); -} - -/** Decodes a `Contact` from the WeaveFFI value-buffer format. */ -inline Contact read_Contact(BufferReader& r) { - Contact out{}; - out.id = r.read_i64(); - out.first_name = r.read_string(); - out.last_name = r.read_string(); - if (r.read_option_flag()) { - std::string v_email_v = r.read_string(); - out.email = std::move(v_email_v); - } - out.contact_type = static_cast(r.read_i32()); - return out; -} - -} // namespace detail - -/** Typed errors reported by the `contacts` module's throwing functions. */ -class ContactsError : public WeaveFFIError { -public: - ContactsError(int32_t code, const std::string& msg) : WeaveFFIError(code, msg) {} -}; - -/** name must not be empty */ -class InvalidNameError : public ContactsError { -public: - InvalidNameError(const std::string& msg) : ContactsError(1, msg) {} -}; - -/** contact not found */ -class NotFoundError : public ContactsError { -public: - NotFoundError(const std::string& msg) : ContactsError(2, msg) {} -}; - -namespace detail { - -/** Map a `contacts` error code and payload to its typed exception (WeaveFFIError for runtime codes, ContactsError for unknown codes). */ -inline std::exception_ptr make_contacts_error(int32_t code, const std::string& msg, const uint8_t* payload_ptr, size_t payload_len) { - if (code < 0) return std::make_exception_ptr(WeaveFFIError(code, msg)); - switch (code) { - case 1: return std::make_exception_ptr(InvalidNameError(msg)); - case 2: return std::make_exception_ptr(NotFoundError(msg)); - default: return std::make_exception_ptr(ContactsError(code, msg)); - } -} - -/** Throw the typed `contacts` exception if `err` carries a nonzero code. */ -inline void check_contacts(weaveffi_error& err) { - if (err.code == 0) return; - std::string msg(err.message ? err.message : "unknown error"); - std::exception_ptr ex = make_contacts_error(err.code, msg, err.payload_ptr, err.payload_len); - weaveffi_error_clear(&err); - std::rethrow_exception(ex); -} - -} // namespace detail - -/** An in-memory address book owning its contacts */ -class ContactBook { - void* handle_; - -public: - explicit ContactBook(void* h) : handle_(h) {} - - ~ContactBook() { - if (handle_) weaveffi_contacts_ContactBook_destroy(static_cast(handle_)); - } - - ContactBook(const ContactBook&) = delete; - ContactBook& operator=(const ContactBook&) = delete; - - ContactBook(ContactBook&& other) noexcept : handle_(other.handle_) { - other.handle_ = nullptr; - } - - ContactBook& operator=(ContactBook&& other) noexcept { - if (this != &other) { - if (handle_) weaveffi_contacts_ContactBook_destroy(static_cast(handle_)); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - void* handle() const { return handle_; } - - ContactBook() : handle_(nullptr) { - weaveffi_error err{}; - auto result = weaveffi_contacts_ContactBook_new(&err); - detail::check(err); - handle_ = result; - } - - Contact add(const std::string& first_name, const std::string& last_name, const std::optional& email, ContactType contact_type) const { - detail::BufferWriter email_buf; - email_buf.write_option_flag(email.has_value()); - if (email.has_value()) { - email_buf.write_string((*email)); - } - size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_contacts_ContactBook_add(static_cast(handle_), first_name.c_str(), last_name.c_str(), email_buf.data(), email_buf.size(), static_cast(static_cast(contact_type)), &out_len, &err); - detail::check_contacts(err); - detail::BufferGuard result_guard{result, out_len}; - detail::BufferReader result_r(result, out_len); - Contact ret = detail::read_Contact(result_r); - result_r.expect_end(); - return ret; - } - - Contact get(int64_t id) const { - size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_contacts_ContactBook_get(static_cast(handle_), id, &out_len, &err); - detail::check_contacts(err); - detail::BufferGuard result_guard{result, out_len}; - detail::BufferReader result_r(result, out_len); - Contact ret = detail::read_Contact(result_r); - result_r.expect_end(); - return ret; - } - - std::vector list() const { - size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_contacts_ContactBook_list(static_cast(handle_), &out_len, &err); - detail::check(err); - detail::BufferGuard result_guard{result, out_len}; - detail::BufferReader result_r(result, out_len); - std::vector ret{}; - { - size_t ret_n = result_r.read_len(); - ret.reserve(ret_n); - for (size_t ret_i = 0; ret_i < ret_n; ++ret_i) { - Contact ret_item = detail::read_Contact(result_r); - ret.push_back(std::move(ret_item)); - } - } - result_r.expect_end(); - return ret; - } - - bool remove(int64_t id) const { - weaveffi_error err{}; - auto result = weaveffi_contacts_ContactBook_remove(static_cast(handle_), id, &err); - detail::check(err); - return result; - } - - int32_t count() const { - weaveffi_error err{}; - auto result = weaveffi_contacts_ContactBook_count(static_cast(handle_), &err); - detail::check(err); - return result; - } - -}; - -} // namespace weaveffi - -// END weaveffi.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__weaveffi_hpp.snap index fdd89779..47214c26 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_docs_everywhere__weaveffi_hpp.snap @@ -49,6 +49,13 @@ source: crates/weaveffi-cli/tests/snapshots.rs extern "C" { +/* The WeaveFFI C ABI revision this header was generated against. The + producer exports weaveffi_abi_version() so a consumer can refuse to load + a library built for a different revision instead of misreading its + error struct or value buffers. */ +#define WEAVEFFI_ABI_VERSION 1u +WEAVEFFI_API uint32_t weaveffi_abi_version(void); + typedef uint64_t weaveffi_handle_t; /* Error slot written by every fallible call. `payload_ptr`/`payload_len` diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_async_demo__CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__CMakeLists_txt.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/cpp_async_demo__CMakeLists_txt.snap rename to crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__CMakeLists_txt.snap diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_async_demo__README_md.snap b/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__README_md.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/cpp_async_demo__README_md.snap rename to crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__README_md.snap diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__weaveffi_hpp.snap new file mode 100644 index 00000000..3489b4a2 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/cpp_edge_cases__weaveffi_hpp.snap @@ -0,0 +1,1664 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from weaveffi.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate weaveffi.yml -o + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef WEAVEFFI_API +# if defined(_WIN32) || defined(__CYGWIN__) +# ifdef WEAVEFFI_BUILD +# define WEAVEFFI_API __declspec(dllexport) +# else +# define WEAVEFFI_API __declspec(dllimport) +# endif +# elif defined(__EMSCRIPTEN__) +# define WEAVEFFI_API __attribute__((used, visibility("default"))) +# elif defined(__GNUC__) && (__GNUC__ >= 4) +# define WEAVEFFI_API __attribute__((visibility("default"))) +# else +# define WEAVEFFI_API +# endif +#endif + +#ifndef WEAVEFFI_DEPRECATED +# if defined(_MSC_VER) +# define WEAVEFFI_DEPRECATED(msg) __declspec(deprecated(msg)) +# elif defined(__GNUC__) || defined(__clang__) +# define WEAVEFFI_DEPRECATED(msg) __attribute__((deprecated(msg))) +# else +# define WEAVEFFI_DEPRECATED(msg) +# endif +#endif + +extern "C" { + +/* The WeaveFFI C ABI revision this header was generated against. The + producer exports weaveffi_abi_version() so a consumer can refuse to load + a library built for a different revision instead of misreading its + error struct or value buffers. */ +#define WEAVEFFI_ABI_VERSION 1u +WEAVEFFI_API uint32_t weaveffi_abi_version(void); + +typedef uint64_t weaveffi_handle_t; + +/* Error slot written by every fallible call. `payload_ptr`/`payload_len` + hold the matched error code's fields serialized in the WeaveFFI value + buffer format (null when the code declares no fields); both the message + and the payload are released by weaveffi_error_clear. */ +typedef struct weaveffi_error { + int32_t code; + const char* message; + const uint8_t* payload_ptr; + size_t payload_len; +} weaveffi_error; + +WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); + +/* Async completion callbacks receive a heap-boxed error the consumer + owns; weaveffi_error_free releases the message, the payload, and the + box itself. Passing NULL is a safe no-op. */ +WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); +WEAVEFFI_API void weaveffi_free_string(const char* ptr); +WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); + +/* Linear-memory allocator used by the Wasm JS glue to stage call + arguments. Native consumers never call these; producers targeting + WebAssembly must export them (the generated weaveffi.c provides + malloc/free-backed defaults). */ +WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size); +WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size); + +typedef struct weaveffi_cancel_token weaveffi_cancel_token; +WEAVEFFI_API weaveffi_cancel_token* weaveffi_cancel_token_create(void); +WEAVEFFI_API void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); +WEAVEFFI_API bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); +WEAVEFFI_API void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); + +/** Deprecated enum with non-contiguous values and keyword-like variants */ +typedef enum { weaveffi_edge_cases_Legacy_None = 0, weaveffi_edge_cases_Legacy_Default = 5, weaveffi_edge_cases_Legacy_Class = 10 } weaveffi_edge_cases_Legacy; +typedef enum { weaveffi_edge_cases_Mode_Fast = 1, weaveffi_edge_cases_Mode_Safe = 2 } weaveffi_edge_cases_Mode; +/** Error codes reported by throwing functions in the `edge.cases` module tree. */ +typedef enum { weaveffi_edge_cases_EdgeErrors_Busy = 7 } weaveffi_edge_cases_EdgeErrors; +typedef struct weaveffi_edge_cases_Widget weaveffi_edge_cases_Widget; +typedef struct weaveffi_edge_cases_Vault weaveffi_edge_cases_Vault; +typedef struct weaveffi_edge_cases_StringStreamIterator weaveffi_edge_cases_StringStreamIterator; +typedef struct weaveffi_edge_cases_ScalarStreamIterator weaveffi_edge_cases_ScalarStreamIterator; +typedef struct weaveffi_edge_cases_ModeStreamIterator weaveffi_edge_cases_ModeStreamIterator; +typedef struct weaveffi_edge_cases_Leaf weaveffi_edge_cases_Leaf; +/** Callback carrying composite payloads */ +typedef void (*weaveffi_edge_cases_OnBatch_fn)(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, weaveffi_edge_cases_Mode mode, void* context); +typedef void (*weaveffi_edge_cases_OnTick_fn)(void* context); +typedef void (*weaveffi_edge_cases_async_void_callback)(void* context, weaveffi_error* err); +typedef void (*weaveffi_edge_cases_async_bytes_callback)(void* context, weaveffi_error* err, const uint8_t* result, size_t result_len); +typedef void (*weaveffi_edge_cases_async_struct_callback)(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*weaveffi_edge_cases_async_list_callback)(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*weaveffi_edge_cases_async_optional_callback)(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len); +typedef void (*weaveffi_edge_cases_async_enum_callback)(void* context, weaveffi_error* err, weaveffi_edge_cases_Mode result); +typedef void (*weaveffi_edge_cases_async_keywords_callback)(void* context, weaveffi_error* err, const char* result); +typedef void (*weaveffi_edge_cases_Widget_fetch_callback)(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len); + +/** Interface with no constructor; instances come from make_widget */ +/** Void method that can fail */ +WEAVEFFI_API void weaveffi_edge_cases_Widget_refresh(const weaveffi_edge_cases_Widget* self, weaveffi_error* out_err); +WEAVEFFI_API const char* weaveffi_edge_cases_Widget_rename(const weaveffi_edge_cases_Widget* self, const char* class_, const uint8_t* import_ptr, size_t import_len, weaveffi_error* out_err); +/** Async method with a struct result */ +WEAVEFFI_API void weaveffi_edge_cases_Widget_fetch_async(const weaveffi_edge_cases_Widget* self, int64_t id, weaveffi_edge_cases_Widget_fetch_callback callback, void* context); +WEAVEFFI_DEPRECATED("Use rename instead") +WEAVEFFI_API const char* weaveffi_edge_cases_Widget_old_name(const weaveffi_edge_cases_Widget* self, weaveffi_error* out_err); +WEAVEFFI_API void weaveffi_edge_cases_Widget_destroy(weaveffi_edge_cases_Widget* self); + +/** Deprecated interface with a throwing constructor */ +/** Fails when the vault is busy */ +WEAVEFFI_API weaveffi_edge_cases_Vault* weaveffi_edge_cases_Vault_open(const char* path, weaveffi_error* out_err); +/** Throwing static */ +WEAVEFFI_API bool weaveffi_edge_cases_Vault_probe(weaveffi_error* out_err); +WEAVEFFI_API weaveffi_edge_cases_Widget* weaveffi_edge_cases_Vault_peek(const weaveffi_edge_cases_Vault* self, weaveffi_error* out_err); +WEAVEFFI_API void weaveffi_edge_cases_Vault_destroy(weaveffi_edge_cases_Vault* self); + +WEAVEFFI_API uint64_t weaveffi_edge_cases_register_batch_listener(weaveffi_edge_cases_OnBatch_fn callback, void* context); +WEAVEFFI_API void weaveffi_edge_cases_unregister_batch_listener(uint64_t id); +WEAVEFFI_API uint64_t weaveffi_edge_cases_register_tick_listener(weaveffi_edge_cases_OnTick_fn callback, void* context); +WEAVEFFI_API void weaveffi_edge_cases_unregister_tick_listener(uint64_t id); +/** No parameters and no result */ +WEAVEFFI_API void weaveffi_edge_cases_noop(weaveffi_error* out_err); +/** Function named after a keyword whose parameters are all keywords */ +WEAVEFFI_API int32_t weaveffi_edge_cases_select(int32_t class_, const char* from, const uint8_t* func_ptr, size_t func_len, const uint8_t* range_ptr, size_t range_len, bool type_, weaveffi_error* out_err); +/** Optional list of optional structs, the deepest legal nesting */ +WEAVEFFI_API const uint8_t* weaveffi_edge_cases_nested_optional(const uint8_t* items_ptr, size_t items_len, size_t* out_len, weaveffi_error* out_err); +WEAVEFFI_API const uint8_t* weaveffi_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, weaveffi_error* out_err); +/** Write-back parameters */ +WEAVEFFI_API void weaveffi_edge_cases_mutable_params(char* text, uint8_t* blob_ptr, size_t blob_len, weaveffi_error* out_err); +/** Factory for an interface with no constructor */ +WEAVEFFI_API weaveffi_edge_cases_Widget* weaveffi_edge_cases_make_widget(const char* label, weaveffi_error* out_err); +/** Optional interface return */ +WEAVEFFI_API weaveffi_edge_cases_Widget* weaveffi_edge_cases_find_widget(int64_t id, weaveffi_error* out_err); +/** Interface parameter, including an optional one */ +WEAVEFFI_API const char* weaveffi_edge_cases_inspect_widget(const weaveffi_edge_cases_Widget* widget, const weaveffi_edge_cases_Widget* fallback, weaveffi_error* out_err); +WEAVEFFI_API weaveffi_edge_cases_Leaf* weaveffi_edge_cases_typed_handle_param(weaveffi_edge_cases_Leaf* token, weaveffi_handle_t raw, weaveffi_error* out_err); +/** Iterator of strings */ +WEAVEFFI_API weaveffi_edge_cases_StringStreamIterator* weaveffi_edge_cases_string_stream(weaveffi_error* out_err); +WEAVEFFI_API int32_t weaveffi_edge_cases_StringStreamIterator_next(weaveffi_edge_cases_StringStreamIterator* iter, const char** out_item, weaveffi_error* out_err); +WEAVEFFI_API void weaveffi_edge_cases_StringStreamIterator_destroy(weaveffi_edge_cases_StringStreamIterator* iter); +/** Iterator of scalars */ +WEAVEFFI_API weaveffi_edge_cases_ScalarStreamIterator* weaveffi_edge_cases_scalar_stream(weaveffi_error* out_err); +WEAVEFFI_API int32_t weaveffi_edge_cases_ScalarStreamIterator_next(weaveffi_edge_cases_ScalarStreamIterator* iter, int32_t* out_item, weaveffi_error* out_err); +WEAVEFFI_API void weaveffi_edge_cases_ScalarStreamIterator_destroy(weaveffi_edge_cases_ScalarStreamIterator* iter); +/** Iterator of enums */ +WEAVEFFI_API weaveffi_edge_cases_ModeStreamIterator* weaveffi_edge_cases_mode_stream(weaveffi_error* out_err); +WEAVEFFI_API int32_t weaveffi_edge_cases_ModeStreamIterator_next(weaveffi_edge_cases_ModeStreamIterator* iter, weaveffi_edge_cases_Mode* out_item, weaveffi_error* out_err); +WEAVEFFI_API void weaveffi_edge_cases_ModeStreamIterator_destroy(weaveffi_edge_cases_ModeStreamIterator* iter); +/** Async with no result */ +WEAVEFFI_API void weaveffi_edge_cases_async_void_async(weaveffi_edge_cases_async_void_callback callback, void* context); +WEAVEFFI_API void weaveffi_edge_cases_async_bytes_async(uint32_t size, weaveffi_edge_cases_async_bytes_callback callback, void* context); +WEAVEFFI_API void weaveffi_edge_cases_async_struct_async(weaveffi_edge_cases_async_struct_callback callback, void* context); +WEAVEFFI_API void weaveffi_edge_cases_async_list_async(weaveffi_cancel_token* cancel_token, weaveffi_edge_cases_async_list_callback callback, void* context); +WEAVEFFI_API void weaveffi_edge_cases_async_optional_async(weaveffi_edge_cases_async_optional_callback callback, void* context); +WEAVEFFI_API void weaveffi_edge_cases_async_enum_async(weaveffi_edge_cases_async_enum_callback callback, void* context); +WEAVEFFI_API void weaveffi_edge_cases_async_keywords_async(const char* class_, int32_t import, weaveffi_edge_cases_async_keywords_callback callback, void* context); +WEAVEFFI_API const uint8_t* weaveffi_edge_cases_keywords_roundtrip(const uint8_t* value_ptr, size_t value_len, size_t* out_len, weaveffi_error* out_err); +WEAVEFFI_DEPRECATED("Use Mode-based APIs") +WEAVEFFI_API const uint8_t* weaveffi_edge_cases_legacy_value(weaveffi_edge_cases_Legacy value, size_t* out_len, weaveffi_error* out_err); + +} // extern "C" + +namespace weaveffi { + +/** Base exception for every error reported through the C ABI. */ +class WeaveFFIError : public std::runtime_error { + int32_t code_; + +public: + WeaveFFIError(int32_t code, const std::string& msg) : std::runtime_error(msg), code_(code) {} + int32_t code() const { return code_; } +}; + +namespace detail { + +/** Throw the generic WeaveFFIError if `err` carries a nonzero code. */ +inline void check(weaveffi_error& err) { + if (err.code == 0) return; + std::string msg(err.message ? err.message : "unknown error"); + int32_t code = err.code; + weaveffi_error_clear(&err); + throw WeaveFFIError(code, msg); +} + +/** Wrap an async-callback error as the generic WeaveFFIError. */ +inline std::exception_ptr make_error(int32_t code, const std::string& msg) { + return std::make_exception_ptr(WeaveFFIError(code, msg)); +} + +} // namespace detail + +namespace detail { + +/** + * Serializes values into the WeaveFFI 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 { return buf_.data(); } + + /** Number of encoded bytes. */ + size_t size() const { 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 string, byte-buffer, or collection length as a u32. */ + void write_len(size_t n) { append_le(static_cast(n)); } + + void write_string(const std::string& 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); } +}; + +/** + * Decodes values from the WeaveFFI value-buffer wire format. A malformed + * buffer is a producer/consumer contract violation (both sides are generated + * from one IDL), so every decode failure throws the generic WeaveFFIError, + * the same channel as a producer panic. + */ +class BufferReader { + const uint8_t* data_; + size_t len_; + size_t pos_; + + [[noreturn]] static void fail(const char* what) { + throw WeaveFFIError(-2, std::string("malformed WeaveFFI value buffer: ") + what); + } + + 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: + BufferReader(const uint8_t* data, size_t len) : data_(data), len_(len), pos_(0) {} + + /** Bytes not yet consumed. */ + size_t remaining() const { 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 length prefix, 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); + } + + 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; + } + + /** 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 weaveffi_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() { + if (ptr != nullptr) weaveffi_free_bytes(const_cast(ptr), len); + } +}; + +} // namespace detail + +/** Deprecated enum with non-contiguous values and keyword-like variants */ +enum class Legacy : int32_t { + None = 0, + Default = 5, + Class = 10 +}; + +enum class Mode : int32_t { + Fast = 1, + Safe = 2 +}; + +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 */ +struct Retired { + int64_t id; +}; + +namespace detail { + +/** Encodes a `Leaf` in the WeaveFFI value-buffer format. */ +inline void write_Leaf(BufferWriter& w, const Leaf& v) { + w.write_f32(v.weight); +} + +/** Decodes a `Leaf` from the WeaveFFI value-buffer format. */ +inline Leaf read_Leaf(BufferReader& r) { + Leaf out{}; + out.weight = r.read_f32(); + return out; +} + +/** Encodes a `Inner` in the WeaveFFI value-buffer format. */ +inline void write_Inner(BufferWriter& w, const Inner& v) { + detail::write_Leaf(w, v.leaf); + w.write_len(v.leaves.size()); + for (const auto& item0 : v.leaves) { + detail::write_Leaf(w, item0); + } +} + +/** Decodes a `Inner` from the WeaveFFI value-buffer format. */ +inline Inner read_Inner(BufferReader& r) { + Inner out{}; + out.leaf = detail::read_Leaf(r); + { + size_t v_leaves_n = r.read_len(); + out.leaves.reserve(v_leaves_n); + for (size_t v_leaves_i = 0; v_leaves_i < v_leaves_n; ++v_leaves_i) { + Leaf v_leaves_item = detail::read_Leaf(r); + out.leaves.push_back(std::move(v_leaves_item)); + } + } + return out; +} + +/** Encodes a `Outer` in the WeaveFFI value-buffer format. */ +inline void write_Outer(BufferWriter& w, const Outer& v) { + detail::write_Inner(w, v.inner); + w.write_option_flag(v.maybe_inner.has_value()); + if (v.maybe_inner.has_value()) { + detail::write_Inner(w, (*v.maybe_inner)); + } + w.write_len(v.by_name.size()); + for (const auto& kv0 : v.by_name) { + w.write_string(kv0.first); + detail::write_Inner(w, kv0.second); + } + w.write_len(v.modes.size()); + for (const auto& item0 : v.modes) { + w.write_i32(static_cast(item0)); + } + w.write_len(v.matrix.size()); + for (const auto& item0 : v.matrix) { + w.write_len(item0.size()); + for (const auto& item1 : item0) { + w.write_i32(item1); + } + } + w.write_len(v.sparse.size()); + for (const auto& item0 : v.sparse) { + w.write_option_flag(item0.has_value()); + if (item0.has_value()) { + w.write_i64((*item0)); + } + } + w.write_option_flag(v.blob.has_value()); + if (v.blob.has_value()) { + w.write_bytes((*v.blob)); + } + w.write_option_flag(v.note.has_value()); + if (v.note.has_value()) { + w.write_string((*v.note)); + } + w.write_len(v.by_id.size()); + for (const auto& kv0 : v.by_id) { + w.write_i64(kv0.first); + w.write_string(kv0.second); + } + w.write_bool(v._hidden); + w.write_u8(v.v2); +} + +/** Decodes a `Outer` from the WeaveFFI value-buffer format. */ +inline Outer read_Outer(BufferReader& r) { + Outer out{}; + out.inner = detail::read_Inner(r); + if (r.read_option_flag()) { + Inner v_maybe_inner_v = detail::read_Inner(r); + out.maybe_inner = std::move(v_maybe_inner_v); + } + { + size_t v_by_name_n = r.read_len(); + for (size_t v_by_name_i = 0; v_by_name_i < v_by_name_n; ++v_by_name_i) { + std::string v_by_name_key = r.read_string(); + Inner v_by_name_val = detail::read_Inner(r); + out.by_name.emplace(std::move(v_by_name_key), std::move(v_by_name_val)); + } + } + { + size_t v_modes_n = r.read_len(); + out.modes.reserve(v_modes_n); + for (size_t v_modes_i = 0; v_modes_i < v_modes_n; ++v_modes_i) { + Mode v_modes_item = static_cast(r.read_i32()); + out.modes.push_back(std::move(v_modes_item)); + } + } + { + size_t v_matrix_n = r.read_len(); + out.matrix.reserve(v_matrix_n); + for (size_t v_matrix_i = 0; v_matrix_i < v_matrix_n; ++v_matrix_i) { + std::vector v_matrix_item{}; + { + size_t v_matrix_item_n = r.read_len(); + v_matrix_item.reserve(v_matrix_item_n); + for (size_t v_matrix_item_i = 0; v_matrix_item_i < v_matrix_item_n; ++v_matrix_item_i) { + int32_t v_matrix_item_item = r.read_i32(); + v_matrix_item.push_back(std::move(v_matrix_item_item)); + } + } + out.matrix.push_back(std::move(v_matrix_item)); + } + } + { + size_t v_sparse_n = r.read_len(); + out.sparse.reserve(v_sparse_n); + for (size_t v_sparse_i = 0; v_sparse_i < v_sparse_n; ++v_sparse_i) { + std::optional v_sparse_item{}; + if (r.read_option_flag()) { + int64_t v_sparse_item_v = r.read_i64(); + v_sparse_item = std::move(v_sparse_item_v); + } + out.sparse.push_back(std::move(v_sparse_item)); + } + } + if (r.read_option_flag()) { + std::vector v_blob_v = r.read_bytes(); + out.blob = std::move(v_blob_v); + } + if (r.read_option_flag()) { + std::string v_note_v = r.read_string(); + out.note = std::move(v_note_v); + } + { + size_t v_by_id_n = r.read_len(); + for (size_t v_by_id_i = 0; v_by_id_i < v_by_id_n; ++v_by_id_i) { + int64_t v_by_id_key = r.read_i64(); + std::string v_by_id_val = r.read_string(); + out.by_id.emplace(std::move(v_by_id_key), std::move(v_by_id_val)); + } + } + out._hidden = r.read_bool(); + out.v2 = r.read_u8(); + return out; +} + +/** Encodes a `Keywords` in the WeaveFFI value-buffer format. */ +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); +} + +/** Decodes a `Keywords` from the WeaveFFI value-buffer format. */ +inline Keywords read_Keywords(BufferReader& r) { + Keywords out{}; + out.class_ = r.read_i32(); + out.def = r.read_i32(); + out.import = r.read_i32(); + out.from = r.read_i32(); + out.func = r.read_i32(); + out.range = r.read_i32(); + out.register_ = r.read_i32(); + out.volatile_ = r.read_i32(); + out.new_ = r.read_i32(); + out.default_ = r.read_i32(); + out.end = r.read_i32(); + out.in = r.read_i32(); + out.is = r.read_i32(); + out.int_ = r.read_i32(); + out.var = r.read_i32(); + out.val = r.read_i32(); + out.self = r.read_i32(); + out.this_ = r.read_i32(); + out.object = r.read_i32(); + out.string = r.read_i32(); + out.case_ = r.read_i32(); + out.select = r.read_i32(); + out.switch_ = r.read_i32(); + out.void_ = r.read_i32(); + out.union_ = r.read_i32(); + out.namespace_ = r.read_i32(); + out.operator_ = r.read_i32(); + out.template_ = r.read_i32(); + out.package = r.read_i32(); + out.protocol = r.read_i32(); + out.guard = r.read_i32(); + out.defer = r.read_i32(); + out.inout = r.read_i32(); + out.internal = r.read_i32(); + out.when = r.read_i32(); + out.fun = r.read_i32(); + out.data = r.read_i32(); + out.late = r.read_i32(); + out.part = r.read_i32(); + out.out = r.read_i32(); + out.ref = r.read_i32(); + out.event = r.read_i32(); + out.base = r.read_i32(); + out.lambda = r.read_i32(); + out.yield = r.read_i32(); + out.nil = r.read_i32(); + out.null = r.read_i32(); + out.begin = r.read_i32(); + out.rescue = r.read_i32(); + out.module = r.read_i32(); + out.chan = r.read_i32(); + out.go = r.read_i32(); + out.let = r.read_i32(); + out.export_ = r.read_i32(); + out.delete_ = r.read_i32(); + out.typedef_ = r.read_i32(); + out.sizeof_ = r.read_i32(); + out.static_ = r.read_i32(); + out.super = r.read_i32(); + return out; +} + +/** Encodes a `Retired` in the WeaveFFI value-buffer format. */ +inline void write_Retired(BufferWriter& w, const Retired& v) { + w.write_i64(v.id); +} + +/** Decodes a `Retired` from the WeaveFFI value-buffer format. */ +inline Retired read_Retired(BufferReader& r) { + Retired out{}; + out.id = r.read_i64(); + return out; +} + +} // namespace detail + +/** Typed errors reported by the `edge_cases` module's throwing functions. */ +class EdgeErrorsError : public WeaveFFIError { +public: + EdgeErrorsError(int32_t code, const std::string& msg) : WeaveFFIError(code, msg) {} +}; + +/** Resource is busy */ +class BusyError : public EdgeErrorsError { +public: + BusyError(const std::string& msg) : EdgeErrorsError(7, msg) {} +}; + +namespace detail { + +/** Map a `edge_cases` error code and payload to its typed exception (WeaveFFIError for runtime codes, EdgeErrorsError for unknown codes). */ +inline std::exception_ptr make_edge_cases_error(int32_t code, const std::string& msg, const uint8_t* payload_ptr, size_t payload_len) { + if (code < 0) return std::make_exception_ptr(WeaveFFIError(code, msg)); + switch (code) { + case 7: return std::make_exception_ptr(BusyError(msg)); + default: return std::make_exception_ptr(EdgeErrorsError(code, msg)); + } +} + +/** Throw the typed `edge_cases` exception if `err` carries a nonzero code. */ +inline void check_edge_cases(weaveffi_error& err) { + if (err.code == 0) return; + std::string msg(err.message ? err.message : "unknown error"); + std::exception_ptr ex = make_edge_cases_error(err.code, msg, err.payload_ptr, err.payload_len); + weaveffi_error_clear(&err); + std::rethrow_exception(ex); +} + +} // namespace detail + +namespace detail { + +inline std::mutex& wv_listener_mutex() { + static std::mutex m; + return m; +} + +inline std::unordered_map>& wv_listener_registry() { + static std::unordered_map> registry; + return registry; +} + +} // namespace detail + +/** Interface with no constructor; instances come from make_widget */ +class Widget { + void* handle_; + +public: + explicit Widget(void* h) : handle_(h) {} + + ~Widget() { + if (handle_) weaveffi_edge_cases_Widget_destroy(static_cast(handle_)); + } + + Widget(const Widget&) = delete; + Widget& operator=(const Widget&) = delete; + + Widget(Widget&& other) noexcept : handle_(other.handle_) { + other.handle_ = nullptr; + } + + Widget& operator=(Widget&& other) noexcept { + if (this != &other) { + if (handle_) weaveffi_edge_cases_Widget_destroy(static_cast(handle_)); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + void* handle() const { return handle_; } + + /** Void method that can fail */ + void refresh() const { + weaveffi_error err{}; + weaveffi_edge_cases_Widget_refresh(static_cast(handle_), &err); + detail::check_edge_cases(err); + } + + std::string rename(const std::string& class_, const std::optional& import) const { + detail::BufferWriter import_buf; + import_buf.write_option_flag(import.has_value()); + if (import.has_value()) { + import_buf.write_string((*import)); + } + weaveffi_error err{}; + auto result = weaveffi_edge_cases_Widget_rename(static_cast(handle_), class_.c_str(), import_buf.data(), import_buf.size(), &err); + detail::check(err); + std::string ret(result); + weaveffi_free_string(result); + return ret; + } + + /** Async method with a struct result */ + std::future fetch(int64_t id) const { + auto* promise_ptr = new std::promise(); + auto future = promise_ptr->get_future(); + weaveffi_edge_cases_Widget_fetch_async(static_cast(handle_), id, [](void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { + auto* p = static_cast*>(context); + if (err && err->code != 0) { + std::string msg(err->message ? err->message : "unknown error"); + p->set_exception(detail::make_edge_cases_error(err->code, msg, err->payload_ptr, err->payload_len)); + weaveffi_error_free(err); + } else { + detail::BufferReader result_r(result_ptr, result_len); + Outer value = detail::read_Outer(result_r); + result_r.expect_end(); + weaveffi_free_bytes(const_cast(result_ptr), result_len); + p->set_value(std::move(value)); + } + delete p; + }, static_cast(promise_ptr)); + return future; + } + + [[deprecated("Use rename instead")]] + std::string old_name() const { + weaveffi_error err{}; + auto result = weaveffi_edge_cases_Widget_old_name(static_cast(handle_), &err); + detail::check(err); + std::string ret(result); + weaveffi_free_string(result); + return ret; + } + +}; + +/** Deprecated interface with a throwing constructor */ +class Vault { + void* handle_; + +public: + explicit Vault(void* h) : handle_(h) {} + + ~Vault() { + if (handle_) weaveffi_edge_cases_Vault_destroy(static_cast(handle_)); + } + + Vault(const Vault&) = delete; + Vault& operator=(const Vault&) = delete; + + Vault(Vault&& other) noexcept : handle_(other.handle_) { + other.handle_ = nullptr; + } + + Vault& operator=(Vault&& other) noexcept { + if (this != &other) { + if (handle_) weaveffi_edge_cases_Vault_destroy(static_cast(handle_)); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + void* handle() const { return handle_; } + + /** Fails when the vault is busy */ + static Vault open(const std::string& path) { + weaveffi_error err{}; + auto result = weaveffi_edge_cases_Vault_open(path.c_str(), &err); + detail::check_edge_cases(err); + return Vault(result); + } + + std::optional peek() const { + weaveffi_error err{}; + auto result = weaveffi_edge_cases_Vault_peek(static_cast(handle_), &err); + detail::check(err); + if (!result) return std::nullopt; + return Widget(result); + } + + /** Throwing static */ + static bool probe() { + weaveffi_error err{}; + auto result = weaveffi_edge_cases_Vault_probe(&err); + detail::check_edge_cases(err); + return result; + } + +}; + +namespace edge_cases { + +/** @return A subscription id for unregister_batch_listener(). */ +inline uint64_t register_batch_listener(std::function, std::vector, std::optional, Mode)> callback) { + auto fn = std::make_shared, std::vector, std::optional, Mode)>>(std::move(callback)); + uint64_t id = weaveffi_edge_cases_register_batch_listener( + [](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, weaveffi_edge_cases_Mode mode, void* context) { + auto& cb = *static_cast, std::vector, std::optional, Mode)>*>(context); + std::vector items_val{}; + if (items_ptr != nullptr) { + detail::BufferReader items_r(items_ptr, items_len); + { + size_t items_val_n = items_r.read_len(); + items_val.reserve(items_val_n); + for (size_t items_val_i = 0; items_val_i < items_val_n; ++items_val_i) { + Outer items_val_item = detail::read_Outer(items_r); + items_val.push_back(std::move(items_val_item)); + } + } + items_r.expect_end(); + } + std::optional first_val{}; + if (first_ptr != nullptr) { + detail::BufferReader first_r(first_ptr, first_len); + if (first_r.read_option_flag()) { + Outer first_val_v = detail::read_Outer(first_r); + first_val = std::move(first_val_v); + } + first_r.expect_end(); + } + cb(std::move(items_val), raw_ptr ? std::vector(raw_ptr, raw_ptr + raw_len) : std::vector{}, std::move(first_val), static_cast(static_cast(mode))); + }, + fn.get()); + std::lock_guard lock(detail::wv_listener_mutex()); + detail::wv_listener_registry()[id] = fn; + return id; +} + +/** Unregisters a listener previously registered with register_batch_listener(). */ +inline void unregister_batch_listener(uint64_t id) { + weaveffi_edge_cases_unregister_batch_listener(id); + std::lock_guard lock(detail::wv_listener_mutex()); + detail::wv_listener_registry().erase(id); +} + +/** @return A subscription id for unregister_tick_listener(). */ +inline uint64_t register_tick_listener(std::function callback) { + auto fn = std::make_shared>(std::move(callback)); + uint64_t id = weaveffi_edge_cases_register_tick_listener( + [](void* context) { + auto& cb = *static_cast*>(context); + cb(); + }, + fn.get()); + std::lock_guard lock(detail::wv_listener_mutex()); + detail::wv_listener_registry()[id] = fn; + return id; +} + +/** Unregisters a listener previously registered with register_tick_listener(). */ +inline void unregister_tick_listener(uint64_t id) { + weaveffi_edge_cases_unregister_tick_listener(id); + std::lock_guard lock(detail::wv_listener_mutex()); + detail::wv_listener_registry().erase(id); +} + +/** No parameters and no result */ +inline void noop() { + weaveffi_error err{}; + weaveffi_edge_cases_noop(&err); + detail::check(err); +} + +/** Function named after a keyword whose parameters are all keywords */ +inline int32_t select(int32_t class_, const std::string& from, const std::vector& func, const std::optional& range, bool type_) { + detail::BufferWriter func_buf; + func_buf.write_len(func.size()); + for (const auto& item0 : func) { + func_buf.write_string(item0); + } + detail::BufferWriter range_buf; + range_buf.write_option_flag(range.has_value()); + if (range.has_value()) { + range_buf.write_i32((*range)); + } + weaveffi_error err{}; + auto result = weaveffi_edge_cases_select(class_, from.c_str(), 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) { + detail::BufferWriter items_buf; + items_buf.write_option_flag(items.has_value()); + if (items.has_value()) { + items_buf.write_len((*items).size()); + for (const auto& item0 : (*items)) { + items_buf.write_option_flag(item0.has_value()); + if (item0.has_value()) { + detail::write_Outer(items_buf, (*item0)); + } + } + } + size_t out_len = 0; + weaveffi_error err{}; + auto result = weaveffi_edge_cases_nested_optional(items_buf.data(), items_buf.size(), &out_len, &err); + detail::check(err); + detail::BufferGuard result_guard{result, out_len}; + detail::BufferReader result_r(result, out_len); + std::optional>> ret{}; + if (result_r.read_option_flag()) { + std::vector> ret_v{}; + { + size_t ret_v_n = result_r.read_len(); + ret_v.reserve(ret_v_n); + for (size_t ret_v_i = 0; ret_v_i < ret_v_n; ++ret_v_i) { + std::optional ret_v_item{}; + if (result_r.read_option_flag()) { + Outer ret_v_item_v = detail::read_Outer(result_r); + ret_v_item = std::move(ret_v_item_v); + } + ret_v.push_back(std::move(ret_v_item)); + } + } + ret = std::move(ret_v); + } + result_r.expect_end(); + return ret; +} + +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) { + detail::BufferWriter text_buf; + text_buf.write_option_flag(text.has_value()); + if (text.has_value()) { + text_buf.write_string((*text)); + } + detail::BufferWriter count_buf; + count_buf.write_option_flag(count.has_value()); + if (count.has_value()) { + count_buf.write_i32((*count)); + } + detail::BufferWriter outer_buf; + outer_buf.write_option_flag(outer.has_value()); + if (outer.has_value()) { + detail::write_Outer(outer_buf, (*outer)); + } + detail::BufferWriter tags_buf; + tags_buf.write_option_flag(tags.has_value()); + if (tags.has_value()) { + tags_buf.write_len((*tags).size()); + for (const auto& item0 : (*tags)) { + tags_buf.write_string(item0); + } + } + detail::BufferWriter blob_buf; + blob_buf.write_option_flag(blob.has_value()); + if (blob.has_value()) { + blob_buf.write_bytes((*blob)); + } + detail::BufferWriter mode_buf; + mode_buf.write_option_flag(mode.has_value()); + if (mode.has_value()) { + mode_buf.write_i32(static_cast((*mode))); + } + size_t out_len = 0; + weaveffi_error err{}; + auto result = weaveffi_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); + detail::BufferGuard result_guard{result, out_len}; + detail::BufferReader result_r(result, out_len); + std::optional ret{}; + if (result_r.read_option_flag()) { + Mode ret_v = static_cast(result_r.read_i32()); + ret = std::move(ret_v); + } + result_r.expect_end(); + return ret; +} + +/** Write-back parameters */ +inline void mutable_params(std::string& text, std::vector& blob) { + weaveffi_error err{}; + weaveffi_edge_cases_mutable_params(text.data(), blob.data(), blob.size(), &err); + detail::check(err); +} + +/** Factory for an interface with no constructor */ +inline Widget make_widget(const std::string& label) { + weaveffi_error err{}; + auto result = weaveffi_edge_cases_make_widget(label.c_str(), &err); + detail::check(err); + return Widget(result); +} + +/** Optional interface return */ +inline std::optional find_widget(int64_t id) { + weaveffi_error err{}; + auto result = weaveffi_edge_cases_find_widget(id, &err); + detail::check_edge_cases(err); + if (!result) return std::nullopt; + return Widget(result); +} + +/** Interface parameter, including an optional one */ +inline std::string inspect_widget(const Widget& widget, const std::optional& fallback) { + weaveffi_error err{}; + auto result = weaveffi_edge_cases_inspect_widget(static_cast(widget.handle()), fallback.has_value() ? static_cast(fallback.value().handle()) : nullptr, &err); + detail::check(err); + std::string ret(result); + weaveffi_free_string(result); + return ret; +} + +inline weaveffi_edge_cases_Leaf* typed_handle_param(weaveffi_edge_cases_Leaf* token, void* raw) { + weaveffi_error err{}; + auto result = weaveffi_edge_cases_typed_handle_param(token, static_cast(reinterpret_cast(raw)), &err); + detail::check(err); + return result; +} + +/** + * A lazy, move-only range over the `std::string` elements produced by `string_stream()`. + * + * Each iteration step pulls exactly one element from the producer, so results stream in constant memory. The range owns the producer-side iterator and releases it exactly once: eagerly when the range is exhausted, or from the destructor when iteration is abandoned early. + */ +class StringStreamIterator { + weaveffi_edge_cases_StringStreamIterator* handle_; + +public: + /** Adopts ownership of the raw producer iterator handle. */ + explicit StringStreamIterator(weaveffi_edge_cases_StringStreamIterator* h) : handle_(h) {} + + ~StringStreamIterator() { + if (handle_) weaveffi_edge_cases_StringStreamIterator_destroy(handle_); + } + + StringStreamIterator(const StringStreamIterator&) = delete; + StringStreamIterator& operator=(const StringStreamIterator&) = delete; + + StringStreamIterator(StringStreamIterator&& other) noexcept : handle_(other.handle_) { + other.handle_ = nullptr; + } + + StringStreamIterator& operator=(StringStreamIterator&& other) noexcept { + if (this != &other) { + if (handle_) weaveffi_edge_cases_StringStreamIterator_destroy(handle_); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + /** Pulls the next element from the producer, or `std::nullopt` once exhausted (which releases the producer iterator eagerly). A producer error throws the generic WeaveFFIError after releasing the iterator. */ + std::optional next() { + if (!handle_) return std::nullopt; + weaveffi_error err{}; + const char* item{}; + int32_t has_item = weaveffi_edge_cases_StringStreamIterator_next(handle_, &item, &err); + if (err.code != 0) { + weaveffi_edge_cases_StringStreamIterator_destroy(handle_); + handle_ = nullptr; + detail::check(err); + } + if (has_item == 0) { + weaveffi_edge_cases_StringStreamIterator_destroy(handle_); + handle_ = nullptr; + return std::nullopt; + } + std::string value(item); + weaveffi_free_string(item); + return value; + } + + /** Sentinel type marking the end of the range. */ + struct sentinel {}; + + /** Single-pass input iterator; each increment pulls one element from the producer. */ + 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 streams one element per iteration step and releases the producer iterator when exhausted or destroyed. + */ +inline StringStreamIterator string_stream() { + weaveffi_error err{}; + weaveffi_edge_cases_StringStreamIterator* iter = weaveffi_edge_cases_string_stream(&err); + detail::check(err); + return StringStreamIterator(iter); +} + +/** + * A lazy, move-only range over the `int32_t` elements produced by `scalar_stream()`. + * + * Each iteration step pulls exactly one element from the producer, so results stream in constant memory. The range owns the producer-side iterator and releases it exactly once: eagerly when the range is exhausted, or from the destructor when iteration is abandoned early. + */ +class ScalarStreamIterator { + weaveffi_edge_cases_ScalarStreamIterator* handle_; + +public: + /** Adopts ownership of the raw producer iterator handle. */ + explicit ScalarStreamIterator(weaveffi_edge_cases_ScalarStreamIterator* h) : handle_(h) {} + + ~ScalarStreamIterator() { + if (handle_) weaveffi_edge_cases_ScalarStreamIterator_destroy(handle_); + } + + ScalarStreamIterator(const ScalarStreamIterator&) = delete; + ScalarStreamIterator& operator=(const ScalarStreamIterator&) = delete; + + ScalarStreamIterator(ScalarStreamIterator&& other) noexcept : handle_(other.handle_) { + other.handle_ = nullptr; + } + + ScalarStreamIterator& operator=(ScalarStreamIterator&& other) noexcept { + if (this != &other) { + if (handle_) weaveffi_edge_cases_ScalarStreamIterator_destroy(handle_); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + /** Pulls the next element from the producer, or `std::nullopt` once exhausted (which releases the producer iterator eagerly). A producer error throws the generic WeaveFFIError after releasing the iterator. */ + std::optional next() { + if (!handle_) return std::nullopt; + weaveffi_error err{}; + int32_t item{}; + int32_t has_item = weaveffi_edge_cases_ScalarStreamIterator_next(handle_, &item, &err); + if (err.code != 0) { + weaveffi_edge_cases_ScalarStreamIterator_destroy(handle_); + handle_ = nullptr; + detail::check(err); + } + if (has_item == 0) { + weaveffi_edge_cases_ScalarStreamIterator_destroy(handle_); + handle_ = nullptr; + return std::nullopt; + } + return item; + } + + /** Sentinel type marking the end of the range. */ + struct sentinel {}; + + /** Single-pass input iterator; each increment pulls one element from the producer. */ + 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 streams one element per iteration step and releases the producer iterator when exhausted or destroyed. + */ +inline ScalarStreamIterator scalar_stream() { + weaveffi_error err{}; + weaveffi_edge_cases_ScalarStreamIterator* iter = weaveffi_edge_cases_scalar_stream(&err); + detail::check(err); + return ScalarStreamIterator(iter); +} + +/** + * A lazy, move-only range over the `Mode` elements produced by `mode_stream()`. + * + * Each iteration step pulls exactly one element from the producer, so results stream in constant memory. The range owns the producer-side iterator and releases it exactly once: eagerly when the range is exhausted, or from the destructor when iteration is abandoned early. + */ +class ModeStreamIterator { + weaveffi_edge_cases_ModeStreamIterator* handle_; + +public: + /** Adopts ownership of the raw producer iterator handle. */ + explicit ModeStreamIterator(weaveffi_edge_cases_ModeStreamIterator* h) : handle_(h) {} + + ~ModeStreamIterator() { + if (handle_) weaveffi_edge_cases_ModeStreamIterator_destroy(handle_); + } + + ModeStreamIterator(const ModeStreamIterator&) = delete; + ModeStreamIterator& operator=(const ModeStreamIterator&) = delete; + + ModeStreamIterator(ModeStreamIterator&& other) noexcept : handle_(other.handle_) { + other.handle_ = nullptr; + } + + ModeStreamIterator& operator=(ModeStreamIterator&& other) noexcept { + if (this != &other) { + if (handle_) weaveffi_edge_cases_ModeStreamIterator_destroy(handle_); + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + /** Pulls the next element from the producer, or `std::nullopt` once exhausted (which releases the producer iterator eagerly). A producer error throws the generic WeaveFFIError after releasing the iterator. */ + std::optional next() { + if (!handle_) return std::nullopt; + weaveffi_error err{}; + weaveffi_edge_cases_Mode item{}; + int32_t has_item = weaveffi_edge_cases_ModeStreamIterator_next(handle_, &item, &err); + if (err.code != 0) { + weaveffi_edge_cases_ModeStreamIterator_destroy(handle_); + handle_ = nullptr; + detail::check(err); + } + if (has_item == 0) { + weaveffi_edge_cases_ModeStreamIterator_destroy(handle_); + handle_ = nullptr; + return std::nullopt; + } + return static_cast(item); + } + + /** Sentinel type marking the end of the range. */ + struct sentinel {}; + + /** Single-pass input iterator; each increment pulls one element from the producer. */ + 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 streams one element per iteration step and releases the producer iterator when exhausted or destroyed. + */ +inline ModeStreamIterator mode_stream() { + weaveffi_error err{}; + weaveffi_edge_cases_ModeStreamIterator* iter = weaveffi_edge_cases_mode_stream(&err); + detail::check(err); + return ModeStreamIterator(iter); +} + +/** Async with no result */ +inline std::future async_void() { + auto* promise_ptr = new std::promise(); + auto future = promise_ptr->get_future(); + weaveffi_edge_cases_async_void_async([](void* context, weaveffi_error* err) { + auto* p = static_cast*>(context); + if (err && err->code != 0) { + std::string msg(err->message ? err->message : "unknown error"); + p->set_exception(detail::make_error(err->code, msg)); + weaveffi_error_free(err); + } else { + p->set_value(); + } + delete p; + }, static_cast(promise_ptr)); + return future; +} + +inline std::future> async_bytes(uint32_t size) { + auto* promise_ptr = new std::promise>(); + auto future = promise_ptr->get_future(); + weaveffi_edge_cases_async_bytes_async(size, [](void* context, weaveffi_error* err, const uint8_t* result, size_t result_len) { + auto* p = static_cast>*>(context); + if (err && err->code != 0) { + std::string msg(err->message ? err->message : "unknown error"); + p->set_exception(detail::make_error(err->code, msg)); + weaveffi_error_free(err); + } else { + std::vector value(result, result + result_len); + weaveffi_free_bytes(const_cast(result), result_len); + p->set_value(std::move(value)); + } + delete p; + }, static_cast(promise_ptr)); + return future; +} + +inline std::future async_struct() { + auto* promise_ptr = new std::promise(); + auto future = promise_ptr->get_future(); + weaveffi_edge_cases_async_struct_async([](void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { + auto* p = static_cast*>(context); + if (err && err->code != 0) { + std::string msg(err->message ? err->message : "unknown error"); + p->set_exception(detail::make_edge_cases_error(err->code, msg, err->payload_ptr, err->payload_len)); + weaveffi_error_free(err); + } else { + detail::BufferReader result_r(result_ptr, result_len); + Outer value = detail::read_Outer(result_r); + result_r.expect_end(); + weaveffi_free_bytes(const_cast(result_ptr), result_len); + p->set_value(std::move(value)); + } + delete p; + }, static_cast(promise_ptr)); + return future; +} + +inline std::future> async_list(weaveffi_cancel_token* cancel_token = nullptr) { + auto* promise_ptr = new std::promise>(); + auto future = promise_ptr->get_future(); + weaveffi_edge_cases_async_list_async(cancel_token, [](void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { + auto* p = static_cast>*>(context); + if (err && err->code != 0) { + std::string msg(err->message ? err->message : "unknown error"); + p->set_exception(detail::make_edge_cases_error(err->code, msg, err->payload_ptr, err->payload_len)); + weaveffi_error_free(err); + } else { + detail::BufferReader result_r(result_ptr, result_len); + std::vector value{}; + { + size_t value_n = result_r.read_len(); + value.reserve(value_n); + for (size_t value_i = 0; value_i < value_n; ++value_i) { + Outer value_item = detail::read_Outer(result_r); + value.push_back(std::move(value_item)); + } + } + result_r.expect_end(); + weaveffi_free_bytes(const_cast(result_ptr), result_len); + p->set_value(std::move(value)); + } + delete p; + }, static_cast(promise_ptr)); + return future; +} + +inline std::future> async_optional() { + auto* promise_ptr = new std::promise>(); + auto future = promise_ptr->get_future(); + weaveffi_edge_cases_async_optional_async([](void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { + auto* p = static_cast>*>(context); + if (err && err->code != 0) { + std::string msg(err->message ? err->message : "unknown error"); + p->set_exception(detail::make_error(err->code, msg)); + weaveffi_error_free(err); + } else { + detail::BufferReader result_r(result_ptr, result_len); + std::optional value{}; + if (result_r.read_option_flag()) { + int64_t value_v = result_r.read_i64(); + value = std::move(value_v); + } + result_r.expect_end(); + weaveffi_free_bytes(const_cast(result_ptr), result_len); + p->set_value(std::move(value)); + } + delete p; + }, static_cast(promise_ptr)); + return future; +} + +inline std::future async_enum() { + auto* promise_ptr = new std::promise(); + auto future = promise_ptr->get_future(); + weaveffi_edge_cases_async_enum_async([](void* context, weaveffi_error* err, weaveffi_edge_cases_Mode result) { + auto* p = static_cast*>(context); + if (err && err->code != 0) { + std::string msg(err->message ? err->message : "unknown error"); + p->set_exception(detail::make_error(err->code, msg)); + weaveffi_error_free(err); + } else { + p->set_value(static_cast(result)); + } + delete p; + }, static_cast(promise_ptr)); + return future; +} + +inline std::future async_keywords(const std::string& class_, int32_t import) { + auto* promise_ptr = new std::promise(); + auto future = promise_ptr->get_future(); + weaveffi_edge_cases_async_keywords_async(class_.c_str(), import, [](void* context, weaveffi_error* err, const char* result) { + auto* p = static_cast*>(context); + if (err && err->code != 0) { + std::string msg(err->message ? err->message : "unknown error"); + p->set_exception(detail::make_error(err->code, msg)); + weaveffi_error_free(err); + } else { + std::string value(result ? result : ""); + weaveffi_free_string(result); + p->set_value(std::move(value)); + } + delete p; + }, static_cast(promise_ptr)); + return future; +} + +inline Keywords keywords_roundtrip(const Keywords& value) { + detail::BufferWriter value_buf; + detail::write_Keywords(value_buf, value); + size_t out_len = 0; + weaveffi_error err{}; + auto result = weaveffi_edge_cases_keywords_roundtrip(value_buf.data(), value_buf.size(), &out_len, &err); + detail::check(err); + detail::BufferGuard result_guard{result, out_len}; + detail::BufferReader result_r(result, out_len); + Keywords ret = detail::read_Keywords(result_r); + result_r.expect_end(); + return ret; +} + +[[deprecated("Use Mode-based APIs")]] +inline Retired legacy_value(Legacy value) { + size_t out_len = 0; + weaveffi_error err{}; + auto result = weaveffi_edge_cases_legacy_value(static_cast(static_cast(value)), &out_len, &err); + detail::check(err); + detail::BufferGuard result_guard{result, out_len}; + detail::BufferReader result_r(result, out_len); + Retired ret = detail::read_Retired(result_r); + result_r.expect_end(); + return ret; +} + +} // namespace edge_cases + +} // namespace weaveffi + +// END weaveffi.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_events__CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/cpp_events__CMakeLists_txt.snap deleted file mode 100644 index f169a251..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_events__CMakeLists_txt.snap +++ /dev/null @@ -1,15 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -cmake_minimum_required(VERSION 3.14) -project(weaveffi_cpp VERSION 0.1.0) -add_library(weaveffi_cpp INTERFACE) -target_include_directories(weaveffi_cpp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(weaveffi_cpp INTERFACE weaveffi) -target_compile_features(weaveffi_cpp INTERFACE cxx_std_17) - -# END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_events__README_md.snap b/crates/weaveffi-cli/tests/snapshots/cpp_events__README_md.snap deleted file mode 100644 index da3ea6c6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_events__README_md.snap +++ /dev/null @@ -1,39 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI C++ Bindings - -## Prerequisites - -- CMake 3.14+ -- C++17 compiler -- The `weaveffi` static/shared library built from the Rust crate - -## Usage with CMake - -Add the generated `cpp/` directory as a subdirectory in your `CMakeLists.txt` and -link against `weaveffi_cpp`: - -```cmake -add_subdirectory(path/to/generated/cpp) -add_executable(myapp main.cpp) -target_link_libraries(myapp weaveffi_cpp) -``` - -The `weaveffi_cpp` target is an INTERFACE library that: - -- Adds the generated header directory to your include path -- Links against the `weaveffi` library -- Requires C++17 - -Then include the header in your code: - -```cpp -#include "weaveffi.hpp" -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_events__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_events__weaveffi_hpp.snap deleted file mode 100644 index b1584d64..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_events__weaveffi_hpp.snap +++ /dev/null @@ -1,274 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef WEAVEFFI_API -# if defined(_WIN32) || defined(__CYGWIN__) -# ifdef WEAVEFFI_BUILD -# define WEAVEFFI_API __declspec(dllexport) -# else -# define WEAVEFFI_API __declspec(dllimport) -# endif -# elif defined(__EMSCRIPTEN__) -# define WEAVEFFI_API __attribute__((used, visibility("default"))) -# elif defined(__GNUC__) && (__GNUC__ >= 4) -# define WEAVEFFI_API __attribute__((visibility("default"))) -# else -# define WEAVEFFI_API -# endif -#endif - -#ifndef WEAVEFFI_DEPRECATED -# if defined(_MSC_VER) -# define WEAVEFFI_DEPRECATED(msg) __declspec(deprecated(msg)) -# elif defined(__GNUC__) || defined(__clang__) -# define WEAVEFFI_DEPRECATED(msg) __attribute__((deprecated(msg))) -# else -# define WEAVEFFI_DEPRECATED(msg) -# endif -#endif - -extern "C" { - -typedef uint64_t weaveffi_handle_t; - -/* Error slot written by every fallible call. `payload_ptr`/`payload_len` - hold the matched error code's fields serialized in the WeaveFFI value - buffer format (null when the code declares no fields); both the message - and the payload are released by weaveffi_error_clear. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); - -/* Async completion callbacks receive a heap-boxed error the consumer - owns; weaveffi_error_free releases the message, the payload, and the - box itself. Passing NULL is a safe no-op. */ -WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); -WEAVEFFI_API void weaveffi_free_string(const char* ptr); -WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); - -/* Linear-memory allocator used by the Wasm JS glue to stage call - arguments. Native consumers never call these; producers targeting - WebAssembly must export them (the generated weaveffi.c provides - malloc/free-backed defaults). */ -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size); -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size); - -typedef struct weaveffi_cancel_token weaveffi_cancel_token; -WEAVEFFI_API weaveffi_cancel_token* weaveffi_cancel_token_create(void); -WEAVEFFI_API void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); -WEAVEFFI_API bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); -WEAVEFFI_API void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); - -typedef struct weaveffi_events_GetMessagesIterator weaveffi_events_GetMessagesIterator; -typedef void (*weaveffi_events_OnMessage_fn)(const char* message, void* context); - -WEAVEFFI_API uint64_t weaveffi_events_register_message_listener(weaveffi_events_OnMessage_fn callback, void* context); -WEAVEFFI_API void weaveffi_events_unregister_message_listener(uint64_t id); -/** Send a message, triggering the OnMessage callback */ -WEAVEFFI_API void weaveffi_events_send_message(const char* text, weaveffi_error* out_err); -/** Return an iterator over all sent messages */ -WEAVEFFI_API weaveffi_events_GetMessagesIterator* weaveffi_events_get_messages(weaveffi_error* out_err); -WEAVEFFI_API int32_t weaveffi_events_GetMessagesIterator_next(weaveffi_events_GetMessagesIterator* iter, const char** out_item, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_events_GetMessagesIterator_destroy(weaveffi_events_GetMessagesIterator* iter); - -} // extern "C" - -namespace weaveffi { - -/** Base exception for every error reported through the C ABI. */ -class WeaveFFIError : public std::runtime_error { - int32_t code_; - -public: - WeaveFFIError(int32_t code, const std::string& msg) : std::runtime_error(msg), code_(code) {} - int32_t code() const { return code_; } -}; - -namespace detail { - -/** Throw the generic WeaveFFIError if `err` carries a nonzero code. */ -inline void check(weaveffi_error& err) { - if (err.code == 0) return; - std::string msg(err.message ? err.message : "unknown error"); - int32_t code = err.code; - weaveffi_error_clear(&err); - throw WeaveFFIError(code, msg); -} - -/** Wrap an async-callback error as the generic WeaveFFIError. */ -inline std::exception_ptr make_error(int32_t code, const std::string& msg) { - return std::make_exception_ptr(WeaveFFIError(code, msg)); -} - -} // namespace detail - -namespace detail { - -inline std::mutex& wv_listener_mutex() { - static std::mutex m; - return m; -} - -inline std::unordered_map>& wv_listener_registry() { - static std::unordered_map> registry; - return registry; -} - -} // namespace detail - -namespace events { - -/** @return A subscription id for unregister_message_listener(). */ -inline uint64_t register_message_listener(std::function callback) { - auto fn = std::make_shared>(std::move(callback)); - uint64_t id = weaveffi_events_register_message_listener( - [](const char* message, void* context) { - auto& cb = *static_cast*>(context); - cb(std::string(message ? message : "")); - }, - fn.get()); - std::lock_guard lock(detail::wv_listener_mutex()); - detail::wv_listener_registry()[id] = fn; - return id; -} - -/** Unregisters a listener previously registered with register_message_listener(). */ -inline void unregister_message_listener(uint64_t id) { - weaveffi_events_unregister_message_listener(id); - std::lock_guard lock(detail::wv_listener_mutex()); - detail::wv_listener_registry().erase(id); -} - -/** Send a message, triggering the OnMessage callback */ -inline void send_message(const std::string& text) { - weaveffi_error err{}; - weaveffi_events_send_message(text.c_str(), &err); - detail::check(err); -} - -/** - * A lazy, move-only range over the `std::string` elements produced by `get_messages()`. - * - * Each iteration step pulls exactly one element from the producer, so results stream in constant memory. The range owns the producer-side iterator and releases it exactly once: eagerly when the range is exhausted, or from the destructor when iteration is abandoned early. - */ -class GetMessagesIterator { - weaveffi_events_GetMessagesIterator* handle_; - -public: - /** Adopts ownership of the raw producer iterator handle. */ - explicit GetMessagesIterator(weaveffi_events_GetMessagesIterator* h) : handle_(h) {} - - ~GetMessagesIterator() { - if (handle_) weaveffi_events_GetMessagesIterator_destroy(handle_); - } - - GetMessagesIterator(const GetMessagesIterator&) = delete; - GetMessagesIterator& operator=(const GetMessagesIterator&) = delete; - - GetMessagesIterator(GetMessagesIterator&& other) noexcept : handle_(other.handle_) { - other.handle_ = nullptr; - } - - GetMessagesIterator& operator=(GetMessagesIterator&& other) noexcept { - if (this != &other) { - if (handle_) weaveffi_events_GetMessagesIterator_destroy(handle_); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - /** Pulls the next element from the producer, or `std::nullopt` once exhausted (which releases the producer iterator eagerly). A producer error throws the generic WeaveFFIError after releasing the iterator. */ - std::optional next() { - if (!handle_) return std::nullopt; - weaveffi_error err{}; - const char* item{}; - int32_t has_item = weaveffi_events_GetMessagesIterator_next(handle_, &item, &err); - if (err.code != 0) { - weaveffi_events_GetMessagesIterator_destroy(handle_); - handle_ = nullptr; - detail::check(err); - } - if (has_item == 0) { - weaveffi_events_GetMessagesIterator_destroy(handle_); - handle_ = nullptr; - return std::nullopt; - } - std::string value(item); - weaveffi_free_string(item); - return value; - } - - /** Sentinel type marking the end of the range. */ - struct sentinel {}; - - /** Single-pass input iterator; each increment pulls one element from the producer. */ - class iterator { - GetMessagesIterator* 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(GetMessagesIterator* 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{}; } -}; - -/** - * Return an iterator over all sent messages - * - * @return A lazy `GetMessagesIterator` range that streams one element per iteration step and releases the producer iterator when exhausted or destroyed. - */ -inline GetMessagesIterator get_messages() { - weaveffi_error err{}; - weaveffi_events_GetMessagesIterator* iter = weaveffi_events_get_messages(&err); - detail::check(err); - return GetMessagesIterator(iter); -} - -} // namespace events - -} // namespace weaveffi - -// END weaveffi.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_inventory__CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/cpp_inventory__CMakeLists_txt.snap deleted file mode 100644 index f169a251..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_inventory__CMakeLists_txt.snap +++ /dev/null @@ -1,15 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -cmake_minimum_required(VERSION 3.14) -project(weaveffi_cpp VERSION 0.1.0) -add_library(weaveffi_cpp INTERFACE) -target_include_directories(weaveffi_cpp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(weaveffi_cpp INTERFACE weaveffi) -target_compile_features(weaveffi_cpp INTERFACE cxx_std_17) - -# END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_inventory__README_md.snap b/crates/weaveffi-cli/tests/snapshots/cpp_inventory__README_md.snap deleted file mode 100644 index da3ea6c6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_inventory__README_md.snap +++ /dev/null @@ -1,39 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI C++ Bindings - -## Prerequisites - -- CMake 3.14+ -- C++17 compiler -- The `weaveffi` static/shared library built from the Rust crate - -## Usage with CMake - -Add the generated `cpp/` directory as a subdirectory in your `CMakeLists.txt` and -link against `weaveffi_cpp`: - -```cmake -add_subdirectory(path/to/generated/cpp) -add_executable(myapp main.cpp) -target_link_libraries(myapp weaveffi_cpp) -``` - -The `weaveffi_cpp` target is an INTERFACE library that: - -- Adds the generated header directory to your include path -- Links against the `weaveffi` library -- Requires C++17 - -Then include the header in your code: - -```cpp -#include "weaveffi.hpp" -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_inventory__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_inventory__weaveffi_hpp.snap deleted file mode 100644 index 3223ff8d..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_inventory__weaveffi_hpp.snap +++ /dev/null @@ -1,650 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef WEAVEFFI_API -# if defined(_WIN32) || defined(__CYGWIN__) -# ifdef WEAVEFFI_BUILD -# define WEAVEFFI_API __declspec(dllexport) -# else -# define WEAVEFFI_API __declspec(dllimport) -# endif -# elif defined(__EMSCRIPTEN__) -# define WEAVEFFI_API __attribute__((used, visibility("default"))) -# elif defined(__GNUC__) && (__GNUC__ >= 4) -# define WEAVEFFI_API __attribute__((visibility("default"))) -# else -# define WEAVEFFI_API -# endif -#endif - -#ifndef WEAVEFFI_DEPRECATED -# if defined(_MSC_VER) -# define WEAVEFFI_DEPRECATED(msg) __declspec(deprecated(msg)) -# elif defined(__GNUC__) || defined(__clang__) -# define WEAVEFFI_DEPRECATED(msg) __attribute__((deprecated(msg))) -# else -# define WEAVEFFI_DEPRECATED(msg) -# endif -#endif - -extern "C" { - -typedef uint64_t weaveffi_handle_t; - -/* Error slot written by every fallible call. `payload_ptr`/`payload_len` - hold the matched error code's fields serialized in the WeaveFFI value - buffer format (null when the code declares no fields); both the message - and the payload are released by weaveffi_error_clear. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); - -/* Async completion callbacks receive a heap-boxed error the consumer - owns; weaveffi_error_free releases the message, the payload, and the - box itself. Passing NULL is a safe no-op. */ -WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); -WEAVEFFI_API void weaveffi_free_string(const char* ptr); -WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); - -/* Linear-memory allocator used by the Wasm JS glue to stage call - arguments. Native consumers never call these; producers targeting - WebAssembly must export them (the generated weaveffi.c provides - malloc/free-backed defaults). */ -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size); -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size); - -typedef struct weaveffi_cancel_token weaveffi_cancel_token; -WEAVEFFI_API weaveffi_cancel_token* weaveffi_cancel_token_create(void); -WEAVEFFI_API void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); -WEAVEFFI_API bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); -WEAVEFFI_API void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); - -typedef enum { weaveffi_products_Category_Electronics = 0, weaveffi_products_Category_Clothing = 1, weaveffi_products_Category_Food = 2, weaveffi_products_Category_Books = 3 } weaveffi_products_Category; -/** Error codes reported by throwing functions in the `products` module tree. */ -typedef enum { weaveffi_products_ProductsError_InvalidPrice = 1, weaveffi_products_ProductsError_ProductNotFound = 2 } weaveffi_products_ProductsError; -/** Error codes reported by throwing functions in the `orders` module tree. */ -typedef enum { weaveffi_orders_OrdersError_OrderNotFound = 1, weaveffi_orders_OrdersError_EmptyOrder = 2 } weaveffi_orders_OrdersError; -typedef struct weaveffi_products_Catalog weaveffi_products_Catalog; - -/** A product catalog owning its products */ -WEAVEFFI_API weaveffi_products_Catalog* weaveffi_products_Catalog_new(weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_products_Catalog_add_product(const weaveffi_products_Catalog* self, const char* name, double price, weaveffi_products_Category category, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_products_Catalog_get_product(const weaveffi_products_Catalog* self, int64_t id, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_products_Catalog_search(const weaveffi_products_Catalog* self, weaveffi_products_Category category, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API bool weaveffi_products_Catalog_update_price(const weaveffi_products_Catalog* self, int64_t id, double price, weaveffi_error* out_err); -WEAVEFFI_API bool weaveffi_products_Catalog_remove(const weaveffi_products_Catalog* self, int64_t id, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_products_Catalog_destroy(weaveffi_products_Catalog* self); - - -WEAVEFFI_API int64_t weaveffi_orders_create_order(const uint8_t* items_ptr, size_t items_len, weaveffi_error* out_err); -WEAVEFFI_API const uint8_t* weaveffi_orders_get_order(int64_t id, size_t* out_len, weaveffi_error* out_err); -WEAVEFFI_API bool weaveffi_orders_cancel_order(int64_t id, weaveffi_error* out_err); -WEAVEFFI_API bool weaveffi_orders_add_product_to_order(int64_t order_id, const uint8_t* product_ptr, size_t product_len, weaveffi_error* out_err); - -} // extern "C" - -namespace weaveffi { - -/** Base exception for every error reported through the C ABI. */ -class WeaveFFIError : public std::runtime_error { - int32_t code_; - -public: - WeaveFFIError(int32_t code, const std::string& msg) : std::runtime_error(msg), code_(code) {} - int32_t code() const { return code_; } -}; - -namespace detail { - -/** Throw the generic WeaveFFIError if `err` carries a nonzero code. */ -inline void check(weaveffi_error& err) { - if (err.code == 0) return; - std::string msg(err.message ? err.message : "unknown error"); - int32_t code = err.code; - weaveffi_error_clear(&err); - throw WeaveFFIError(code, msg); -} - -/** Wrap an async-callback error as the generic WeaveFFIError. */ -inline std::exception_ptr make_error(int32_t code, const std::string& msg) { - return std::make_exception_ptr(WeaveFFIError(code, msg)); -} - -} // namespace detail - -namespace detail { - -/** - * Serializes values into the WeaveFFI 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 { return buf_.data(); } - - /** Number of encoded bytes. */ - size_t size() const { 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 string, byte-buffer, or collection length as a u32. */ - void write_len(size_t n) { append_le(static_cast(n)); } - - void write_string(const std::string& 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); } -}; - -/** - * Decodes values from the WeaveFFI value-buffer wire format. A malformed - * buffer is a producer/consumer contract violation (both sides are generated - * from one IDL), so every decode failure throws the generic WeaveFFIError, - * the same channel as a producer panic. - */ -class BufferReader { - const uint8_t* data_; - size_t len_; - size_t pos_; - - [[noreturn]] static void fail(const char* what) { - throw WeaveFFIError(-2, std::string("malformed WeaveFFI value buffer: ") + what); - } - - 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: - BufferReader(const uint8_t* data, size_t len) : data_(data), len_(len), pos_(0) {} - - /** Bytes not yet consumed. */ - size_t remaining() const { 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 length prefix, 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); - } - - 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; - } - - /** 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 weaveffi_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() { - if (ptr != nullptr) weaveffi_free_bytes(const_cast(ptr), len); - } -}; - -} // namespace detail - -enum class Category : int32_t { - Electronics = 0, - Clothing = 1, - Food = 2, - Books = 3 -}; - -struct Product { - int64_t id; - std::string name; - std::optional description; - double price; - Category category; - std::vector tags; -}; - -struct OrderItem { - int64_t product_id; - int32_t quantity; - double unit_price; -}; - -struct Order { - int64_t id; - std::vector items; - double total; - std::string status; -}; - -namespace detail { - -/** Encodes a `Product` in the WeaveFFI value-buffer format. */ -inline void write_Product(BufferWriter& w, const Product& v) { - w.write_i64(v.id); - w.write_string(v.name); - w.write_option_flag(v.description.has_value()); - if (v.description.has_value()) { - w.write_string((*v.description)); - } - w.write_f64(v.price); - w.write_i32(static_cast(v.category)); - w.write_len(v.tags.size()); - for (const auto& item0 : v.tags) { - w.write_string(item0); - } -} - -/** Decodes a `Product` from the WeaveFFI value-buffer format. */ -inline Product read_Product(BufferReader& r) { - Product out{}; - out.id = r.read_i64(); - out.name = r.read_string(); - if (r.read_option_flag()) { - std::string v_description_v = r.read_string(); - out.description = std::move(v_description_v); - } - out.price = r.read_f64(); - out.category = static_cast(r.read_i32()); - { - size_t v_tags_n = r.read_len(); - out.tags.reserve(v_tags_n); - for (size_t v_tags_i = 0; v_tags_i < v_tags_n; ++v_tags_i) { - std::string v_tags_item = r.read_string(); - out.tags.push_back(std::move(v_tags_item)); - } - } - return out; -} - -/** Encodes a `OrderItem` in the WeaveFFI value-buffer format. */ -inline void write_OrderItem(BufferWriter& w, const OrderItem& v) { - w.write_i64(v.product_id); - w.write_i32(v.quantity); - w.write_f64(v.unit_price); -} - -/** Decodes a `OrderItem` from the WeaveFFI value-buffer format. */ -inline OrderItem read_OrderItem(BufferReader& r) { - OrderItem out{}; - out.product_id = r.read_i64(); - out.quantity = r.read_i32(); - out.unit_price = r.read_f64(); - return out; -} - -/** Encodes a `Order` in the WeaveFFI value-buffer format. */ -inline void write_Order(BufferWriter& w, const Order& v) { - w.write_i64(v.id); - w.write_len(v.items.size()); - for (const auto& item0 : v.items) { - detail::write_OrderItem(w, item0); - } - w.write_f64(v.total); - w.write_string(v.status); -} - -/** Decodes a `Order` from the WeaveFFI value-buffer format. */ -inline Order read_Order(BufferReader& r) { - Order out{}; - out.id = r.read_i64(); - { - size_t v_items_n = r.read_len(); - out.items.reserve(v_items_n); - for (size_t v_items_i = 0; v_items_i < v_items_n; ++v_items_i) { - OrderItem v_items_item = detail::read_OrderItem(r); - out.items.push_back(std::move(v_items_item)); - } - } - out.total = r.read_f64(); - out.status = r.read_string(); - return out; -} - -} // namespace detail - -/** Typed errors reported by the `products` module's throwing functions. */ -class ProductsError : public WeaveFFIError { -public: - ProductsError(int32_t code, const std::string& msg) : WeaveFFIError(code, msg) {} -}; - -/** price must be positive */ -class InvalidPriceError : public ProductsError { -public: - InvalidPriceError(const std::string& msg) : ProductsError(1, msg) {} -}; - -/** product not found */ -class ProductNotFoundError : public ProductsError { -public: - ProductNotFoundError(const std::string& msg) : ProductsError(2, msg) {} -}; - -namespace detail { - -/** Map a `products` error code and payload to its typed exception (WeaveFFIError for runtime codes, ProductsError for unknown codes). */ -inline std::exception_ptr make_products_error(int32_t code, const std::string& msg, const uint8_t* payload_ptr, size_t payload_len) { - if (code < 0) return std::make_exception_ptr(WeaveFFIError(code, msg)); - switch (code) { - case 1: return std::make_exception_ptr(InvalidPriceError(msg)); - case 2: return std::make_exception_ptr(ProductNotFoundError(msg)); - default: return std::make_exception_ptr(ProductsError(code, msg)); - } -} - -/** Throw the typed `products` exception if `err` carries a nonzero code. */ -inline void check_products(weaveffi_error& err) { - if (err.code == 0) return; - std::string msg(err.message ? err.message : "unknown error"); - std::exception_ptr ex = make_products_error(err.code, msg, err.payload_ptr, err.payload_len); - weaveffi_error_clear(&err); - std::rethrow_exception(ex); -} - -} // namespace detail - -/** Typed errors reported by the `orders` module's throwing functions. */ -class OrdersError : public WeaveFFIError { -public: - OrdersError(int32_t code, const std::string& msg) : WeaveFFIError(code, msg) {} -}; - -/** order not found */ -class OrderNotFoundError : public OrdersError { -public: - OrderNotFoundError(const std::string& msg) : OrdersError(1, msg) {} -}; - -/** order must contain at least one item */ -class EmptyOrderError : public OrdersError { -public: - EmptyOrderError(const std::string& msg) : OrdersError(2, msg) {} -}; - -namespace detail { - -/** Map a `orders` error code and payload to its typed exception (WeaveFFIError for runtime codes, OrdersError for unknown codes). */ -inline std::exception_ptr make_orders_error(int32_t code, const std::string& msg, const uint8_t* payload_ptr, size_t payload_len) { - if (code < 0) return std::make_exception_ptr(WeaveFFIError(code, msg)); - switch (code) { - case 1: return std::make_exception_ptr(OrderNotFoundError(msg)); - case 2: return std::make_exception_ptr(EmptyOrderError(msg)); - default: return std::make_exception_ptr(OrdersError(code, msg)); - } -} - -/** Throw the typed `orders` exception if `err` carries a nonzero code. */ -inline void check_orders(weaveffi_error& err) { - if (err.code == 0) return; - std::string msg(err.message ? err.message : "unknown error"); - std::exception_ptr ex = make_orders_error(err.code, msg, err.payload_ptr, err.payload_len); - weaveffi_error_clear(&err); - std::rethrow_exception(ex); -} - -} // namespace detail - -/** A product catalog owning its products */ -class Catalog { - void* handle_; - -public: - explicit Catalog(void* h) : handle_(h) {} - - ~Catalog() { - if (handle_) weaveffi_products_Catalog_destroy(static_cast(handle_)); - } - - Catalog(const Catalog&) = delete; - Catalog& operator=(const Catalog&) = delete; - - Catalog(Catalog&& other) noexcept : handle_(other.handle_) { - other.handle_ = nullptr; - } - - Catalog& operator=(Catalog&& other) noexcept { - if (this != &other) { - if (handle_) weaveffi_products_Catalog_destroy(static_cast(handle_)); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - void* handle() const { return handle_; } - - Catalog() : handle_(nullptr) { - weaveffi_error err{}; - auto result = weaveffi_products_Catalog_new(&err); - detail::check(err); - handle_ = result; - } - - Product add_product(const std::string& name, double price, Category category) const { - size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_products_Catalog_add_product(static_cast(handle_), name.c_str(), price, static_cast(static_cast(category)), &out_len, &err); - detail::check_products(err); - detail::BufferGuard result_guard{result, out_len}; - detail::BufferReader result_r(result, out_len); - Product ret = detail::read_Product(result_r); - result_r.expect_end(); - return ret; - } - - Product get_product(int64_t id) const { - size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_products_Catalog_get_product(static_cast(handle_), id, &out_len, &err); - detail::check_products(err); - detail::BufferGuard result_guard{result, out_len}; - detail::BufferReader result_r(result, out_len); - Product ret = detail::read_Product(result_r); - result_r.expect_end(); - return ret; - } - - std::vector search(Category category) const { - size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_products_Catalog_search(static_cast(handle_), static_cast(static_cast(category)), &out_len, &err); - detail::check(err); - detail::BufferGuard result_guard{result, out_len}; - detail::BufferReader result_r(result, out_len); - std::vector ret{}; - { - size_t ret_n = result_r.read_len(); - ret.reserve(ret_n); - for (size_t ret_i = 0; ret_i < ret_n; ++ret_i) { - Product ret_item = detail::read_Product(result_r); - ret.push_back(std::move(ret_item)); - } - } - result_r.expect_end(); - return ret; - } - - bool update_price(int64_t id, double price) const { - weaveffi_error err{}; - auto result = weaveffi_products_Catalog_update_price(static_cast(handle_), id, price, &err); - detail::check_products(err); - return result; - } - - bool remove(int64_t id) const { - weaveffi_error err{}; - auto result = weaveffi_products_Catalog_remove(static_cast(handle_), id, &err); - detail::check(err); - return result; - } - -}; - -namespace orders { - -inline int64_t create_order(const std::vector& items) { - detail::BufferWriter items_buf; - items_buf.write_len(items.size()); - for (const auto& item0 : items) { - detail::write_OrderItem(items_buf, item0); - } - weaveffi_error err{}; - auto result = weaveffi_orders_create_order(items_buf.data(), items_buf.size(), &err); - detail::check_orders(err); - return result; -} - -inline Order get_order(int64_t id) { - size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_orders_get_order(id, &out_len, &err); - detail::check_orders(err); - detail::BufferGuard result_guard{result, out_len}; - detail::BufferReader result_r(result, out_len); - Order ret = detail::read_Order(result_r); - result_r.expect_end(); - return ret; -} - -inline bool cancel_order(int64_t id) { - weaveffi_error err{}; - auto result = weaveffi_orders_cancel_order(id, &err); - detail::check(err); - return result; -} - -inline bool add_product_to_order(int64_t order_id, const Product& product) { - detail::BufferWriter product_buf; - detail::write_Product(product_buf, product); - weaveffi_error err{}; - auto result = weaveffi_orders_add_product_to_order(order_id, product_buf.data(), product_buf.size(), &err); - detail::check(err); - return result; -} - -} // namespace orders - -} // namespace weaveffi - -// END weaveffi.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__weaveffi_hpp.snap index e7bc99e9..cfc2233e 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_kitchen_sink__weaveffi_hpp.snap @@ -51,6 +51,13 @@ source: crates/weaveffi-cli/tests/snapshots.rs extern "C" { +/* The WeaveFFI C ABI revision this header was generated against. The + producer exports weaveffi_abi_version() so a consumer can refuse to load + a library built for a different revision instead of misreading its + error struct or value buffers. */ +#define WEAVEFFI_ABI_VERSION 1u +WEAVEFFI_API uint32_t weaveffi_abi_version(void); + typedef uint64_t weaveffi_handle_t; /* Error slot written by every fallible call. `payload_ptr`/`payload_len` diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_kvstore__CMakeLists_txt.snap b/crates/weaveffi-cli/tests/snapshots/cpp_kvstore__CMakeLists_txt.snap deleted file mode 100644 index f169a251..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_kvstore__CMakeLists_txt.snap +++ /dev/null @@ -1,15 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -cmake_minimum_required(VERSION 3.14) -project(weaveffi_cpp VERSION 0.1.0) -add_library(weaveffi_cpp INTERFACE) -target_include_directories(weaveffi_cpp INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(weaveffi_cpp INTERFACE weaveffi) -target_compile_features(weaveffi_cpp INTERFACE cxx_std_17) - -# END CMakeLists.txt diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_kvstore__README_md.snap b/crates/weaveffi-cli/tests/snapshots/cpp_kvstore__README_md.snap deleted file mode 100644 index da3ea6c6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_kvstore__README_md.snap +++ /dev/null @@ -1,39 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI C++ Bindings - -## Prerequisites - -- CMake 3.14+ -- C++17 compiler -- The `weaveffi` static/shared library built from the Rust crate - -## Usage with CMake - -Add the generated `cpp/` directory as a subdirectory in your `CMakeLists.txt` and -link against `weaveffi_cpp`: - -```cmake -add_subdirectory(path/to/generated/cpp) -add_executable(myapp main.cpp) -target_link_libraries(myapp weaveffi_cpp) -``` - -The `weaveffi_cpp` target is an INTERFACE library that: - -- Adds the generated header directory to your include path -- Links against the `weaveffi` library -- Requires C++17 - -Then include the header in your code: - -```cpp -#include "weaveffi.hpp" -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_kvstore__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_kvstore__weaveffi_hpp.snap deleted file mode 100644 index a1a7caa7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/cpp_kvstore__weaveffi_hpp.snap +++ /dev/null @@ -1,806 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef WEAVEFFI_API -# if defined(_WIN32) || defined(__CYGWIN__) -# ifdef WEAVEFFI_BUILD -# define WEAVEFFI_API __declspec(dllexport) -# else -# define WEAVEFFI_API __declspec(dllimport) -# endif -# elif defined(__EMSCRIPTEN__) -# define WEAVEFFI_API __attribute__((used, visibility("default"))) -# elif defined(__GNUC__) && (__GNUC__ >= 4) -# define WEAVEFFI_API __attribute__((visibility("default"))) -# else -# define WEAVEFFI_API -# endif -#endif - -#ifndef WEAVEFFI_DEPRECATED -# if defined(_MSC_VER) -# define WEAVEFFI_DEPRECATED(msg) __declspec(deprecated(msg)) -# elif defined(__GNUC__) || defined(__clang__) -# define WEAVEFFI_DEPRECATED(msg) __attribute__((deprecated(msg))) -# else -# define WEAVEFFI_DEPRECATED(msg) -# endif -#endif - -extern "C" { - -typedef uint64_t weaveffi_handle_t; - -/* Error slot written by every fallible call. `payload_ptr`/`payload_len` - hold the matched error code's fields serialized in the WeaveFFI value - buffer format (null when the code declares no fields); both the message - and the payload are released by weaveffi_error_clear. */ -typedef struct weaveffi_error { - int32_t code; - const char* message; - const uint8_t* payload_ptr; - size_t payload_len; -} weaveffi_error; - -WEAVEFFI_API void weaveffi_error_clear(weaveffi_error* err); - -/* Async completion callbacks receive a heap-boxed error the consumer - owns; weaveffi_error_free releases the message, the payload, and the - box itself. Passing NULL is a safe no-op. */ -WEAVEFFI_API void weaveffi_error_free(weaveffi_error* err); -WEAVEFFI_API void weaveffi_free_string(const char* ptr); -WEAVEFFI_API void weaveffi_free_bytes(uint8_t* ptr, size_t len); - -/* Linear-memory allocator used by the Wasm JS glue to stage call - arguments. Native consumers never call these; producers targeting - WebAssembly must export them (the generated weaveffi.c provides - malloc/free-backed defaults). */ -WEAVEFFI_API uint8_t* weaveffi_alloc(uint32_t size); -WEAVEFFI_API void weaveffi_dealloc(uint8_t* ptr, uint32_t size); - -typedef struct weaveffi_cancel_token weaveffi_cancel_token; -WEAVEFFI_API weaveffi_cancel_token* weaveffi_cancel_token_create(void); -WEAVEFFI_API void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token); -WEAVEFFI_API bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token); -WEAVEFFI_API void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token); - -/** Persistence semantics applied to a stored entry */ -typedef enum { - /** In-memory only; lost on close */ - weaveffi_kv_EntryKind_Volatile = 0, - /** Flushed to durable storage */ - weaveffi_kv_EntryKind_Persistent = 1, - /** Persistent and encrypted at rest */ - weaveffi_kv_EntryKind_Encrypted = 2 -} weaveffi_kv_EntryKind; -/** Error codes reported by throwing functions in the `kv` module tree. */ -typedef enum { - /** No entry exists for the requested key */ - weaveffi_kv_KvError_KeyNotFound = 1001, - /** The entry exists but its TTL has elapsed */ - weaveffi_kv_KvError_Expired = 1002, - /** The store has reached its capacity limit */ - weaveffi_kv_KvError_StoreFull = 1003, - /** Underlying storage reported an I/O failure */ - weaveffi_kv_KvError_IoError = 1004 -} weaveffi_kv_KvError; -typedef struct weaveffi_kv_Store weaveffi_kv_Store; -typedef struct weaveffi_kv_Store_ListKeysIterator weaveffi_kv_Store_ListKeysIterator; -/** Fires when an entry is evicted from the store */ -typedef void (*weaveffi_kv_OnEvict_fn)(const char* key, void* context); -typedef void (*weaveffi_kv_Store_compact_callback)(void* context, weaveffi_error* err, int64_t result); - -/** An in-memory key-value store owning its entries; destroying the object releases all of its state */ -/** Open (or create) a store backed by the given filesystem path */ -WEAVEFFI_API weaveffi_kv_Store* weaveffi_kv_Store_open(const char* path, weaveffi_error* out_err); -/** The largest number of live entries one store will hold */ -WEAVEFFI_API int64_t weaveffi_kv_Store_default_capacity(weaveffi_error* out_err); -/** Insert or replace a value, returning true on success */ -WEAVEFFI_API bool weaveffi_kv_Store_put(const weaveffi_kv_Store* self, const char* key, const uint8_t* value_ptr, size_t value_len, weaveffi_kv_EntryKind kind, const uint8_t* ttl_seconds_ptr, size_t ttl_seconds_len, weaveffi_error* out_err); -/** Look up an entry by key; returns null if missing or expired */ -WEAVEFFI_API const uint8_t* weaveffi_kv_Store_get(const weaveffi_kv_Store* self, const char* key, size_t* out_len, weaveffi_error* out_err); -/** Remove the entry for the given key, returning true if it existed */ -WEAVEFFI_API bool weaveffi_kv_Store_delete(const weaveffi_kv_Store* self, const char* key, weaveffi_error* out_err); -/** Stream every key, optionally filtered by a prefix */ -WEAVEFFI_API weaveffi_kv_Store_ListKeysIterator* weaveffi_kv_Store_list_keys(const weaveffi_kv_Store* self, const uint8_t* prefix_ptr, size_t prefix_len, weaveffi_error* out_err); -WEAVEFFI_API int32_t weaveffi_kv_Store_ListKeysIterator_next(weaveffi_kv_Store_ListKeysIterator* iter, const char** out_item, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_kv_Store_ListKeysIterator_destroy(weaveffi_kv_Store_ListKeysIterator* iter); -/** Return the number of live entries in the store */ -WEAVEFFI_API int64_t weaveffi_kv_Store_count(const weaveffi_kv_Store* self, weaveffi_error* out_err); -/** Drop every entry from the store */ -WEAVEFFI_API void weaveffi_kv_Store_clear(const weaveffi_kv_Store* self, weaveffi_error* out_err); -/** Reclaim space asynchronously; returns the number of bytes reclaimed */ -WEAVEFFI_API void weaveffi_kv_Store_compact_async(const weaveffi_kv_Store* self, weaveffi_cancel_token* cancel_token, weaveffi_kv_Store_compact_callback callback, void* context); -/** Legacy single-shot put kept for compatibility */ -WEAVEFFI_DEPRECATED("use put() with explicit kind") -WEAVEFFI_API bool weaveffi_kv_Store_legacy_put(const weaveffi_kv_Store* self, const char* key, const uint8_t* value_ptr, size_t value_len, weaveffi_error* out_err); -WEAVEFFI_API void weaveffi_kv_Store_destroy(weaveffi_kv_Store* self); - -/** Subscribe to per-key eviction notifications */ -WEAVEFFI_API uint64_t weaveffi_kv_register_eviction_listener(weaveffi_kv_OnEvict_fn callback, void* context); -/** Subscribe to per-key eviction notifications */ -WEAVEFFI_API void weaveffi_kv_unregister_eviction_listener(uint64_t id); - -/** Snapshot the current store statistics */ -WEAVEFFI_API const uint8_t* weaveffi_kv_stats_get_stats(const weaveffi_kv_Store* store, size_t* out_len, weaveffi_error* out_err); - -} // extern "C" - -namespace weaveffi { - -/** Base exception for every error reported through the C ABI. */ -class WeaveFFIError : public std::runtime_error { - int32_t code_; - -public: - WeaveFFIError(int32_t code, const std::string& msg) : std::runtime_error(msg), code_(code) {} - int32_t code() const { return code_; } -}; - -namespace detail { - -/** Throw the generic WeaveFFIError if `err` carries a nonzero code. */ -inline void check(weaveffi_error& err) { - if (err.code == 0) return; - std::string msg(err.message ? err.message : "unknown error"); - int32_t code = err.code; - weaveffi_error_clear(&err); - throw WeaveFFIError(code, msg); -} - -/** Wrap an async-callback error as the generic WeaveFFIError. */ -inline std::exception_ptr make_error(int32_t code, const std::string& msg) { - return std::make_exception_ptr(WeaveFFIError(code, msg)); -} - -} // namespace detail - -namespace detail { - -/** - * Serializes values into the WeaveFFI 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 { return buf_.data(); } - - /** Number of encoded bytes. */ - size_t size() const { 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 string, byte-buffer, or collection length as a u32. */ - void write_len(size_t n) { append_le(static_cast(n)); } - - void write_string(const std::string& 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); } -}; - -/** - * Decodes values from the WeaveFFI value-buffer wire format. A malformed - * buffer is a producer/consumer contract violation (both sides are generated - * from one IDL), so every decode failure throws the generic WeaveFFIError, - * the same channel as a producer panic. - */ -class BufferReader { - const uint8_t* data_; - size_t len_; - size_t pos_; - - [[noreturn]] static void fail(const char* what) { - throw WeaveFFIError(-2, std::string("malformed WeaveFFI value buffer: ") + what); - } - - 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: - BufferReader(const uint8_t* data, size_t len) : data_(data), len_(len), pos_(0) {} - - /** Bytes not yet consumed. */ - size_t remaining() const { 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 length prefix, 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); - } - - 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; - } - - /** 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 weaveffi_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() { - if (ptr != nullptr) weaveffi_free_bytes(const_cast(ptr), len); - } -}; - -} // namespace detail - -/** Persistence semantics applied to a stored entry */ -enum class EntryKind : int32_t { - /** In-memory only; lost on close */ - Volatile = 0, - /** Flushed to durable storage */ - Persistent = 1, - /** Persistent and encrypted at rest */ - Encrypted = 2 -}; - -/** A single key-value entry persisted in the store */ -struct Entry { - /** Stable monotonic identifier assigned on insert */ - int64_t id; - /** UTF-8 lookup key */ - std::string key; - /** Opaque binary payload */ - std::vector value; - /** Unix-timestamp seconds when the entry was created */ - int64_t created_at; - /** Optional unix-timestamp seconds at which the entry expires */ - std::optional expires_at; - /** Free-form labels attached to the entry */ - std::vector tags; - /** Arbitrary string-valued metadata pairs */ - std::unordered_map metadata; -}; - -/** Aggregate store statistics */ -struct Stats { - /** Number of live entries */ - int64_t total_entries; - /** Sum of all value byte lengths */ - int64_t total_bytes; - /** Number of entries past their TTL but not yet evicted */ - int64_t expired_entries; -}; - -namespace detail { - -/** Encodes a `Entry` in the WeaveFFI value-buffer format. */ -inline void write_Entry(BufferWriter& w, const Entry& v) { - w.write_i64(v.id); - w.write_string(v.key); - w.write_bytes(v.value); - w.write_i64(v.created_at); - w.write_option_flag(v.expires_at.has_value()); - if (v.expires_at.has_value()) { - w.write_i64((*v.expires_at)); - } - w.write_len(v.tags.size()); - for (const auto& item0 : v.tags) { - w.write_string(item0); - } - w.write_len(v.metadata.size()); - for (const auto& kv0 : v.metadata) { - w.write_string(kv0.first); - w.write_string(kv0.second); - } -} - -/** Decodes a `Entry` from the WeaveFFI value-buffer format. */ -inline Entry read_Entry(BufferReader& r) { - Entry out{}; - out.id = r.read_i64(); - out.key = r.read_string(); - out.value = r.read_bytes(); - out.created_at = r.read_i64(); - if (r.read_option_flag()) { - int64_t v_expires_at_v = r.read_i64(); - out.expires_at = std::move(v_expires_at_v); - } - { - size_t v_tags_n = r.read_len(); - out.tags.reserve(v_tags_n); - for (size_t v_tags_i = 0; v_tags_i < v_tags_n; ++v_tags_i) { - std::string v_tags_item = r.read_string(); - out.tags.push_back(std::move(v_tags_item)); - } - } - { - size_t v_metadata_n = r.read_len(); - for (size_t v_metadata_i = 0; v_metadata_i < v_metadata_n; ++v_metadata_i) { - std::string v_metadata_key = r.read_string(); - std::string v_metadata_val = r.read_string(); - out.metadata.emplace(std::move(v_metadata_key), std::move(v_metadata_val)); - } - } - return out; -} - -/** Encodes a `Stats` in the WeaveFFI value-buffer format. */ -inline void write_Stats(BufferWriter& w, const Stats& v) { - w.write_i64(v.total_entries); - w.write_i64(v.total_bytes); - w.write_i64(v.expired_entries); -} - -/** Decodes a `Stats` from the WeaveFFI value-buffer format. */ -inline Stats read_Stats(BufferReader& r) { - Stats out{}; - out.total_entries = r.read_i64(); - out.total_bytes = r.read_i64(); - out.expired_entries = r.read_i64(); - return out; -} - -} // namespace detail - -/** Typed errors reported by the `kv` module's throwing functions. */ -class KvError : public WeaveFFIError { -public: - KvError(int32_t code, const std::string& msg) : WeaveFFIError(code, msg) {} -}; - -/** No entry exists for the requested key */ -class KeyNotFoundError : public KvError { -public: - KeyNotFoundError(const std::string& msg) : KvError(1001, msg) {} -}; - -/** The entry exists but its TTL has elapsed */ -class ExpiredError : public KvError { -public: - ExpiredError(const std::string& msg) : KvError(1002, msg) {} -}; - -/** The store has reached its capacity limit */ -class StoreFullError : public KvError { -public: - StoreFullError(const std::string& msg) : KvError(1003, msg) {} -}; - -/** Underlying storage reported an I/O failure */ -class IoError : public KvError { -public: - IoError(const std::string& msg) : KvError(1004, msg) {} -}; - -namespace detail { - -/** Map a `kv` error code and payload to its typed exception (WeaveFFIError for runtime codes, KvError for unknown codes). */ -inline std::exception_ptr make_kv_error(int32_t code, const std::string& msg, const uint8_t* payload_ptr, size_t payload_len) { - if (code < 0) return std::make_exception_ptr(WeaveFFIError(code, msg)); - switch (code) { - case 1001: return std::make_exception_ptr(KeyNotFoundError(msg)); - case 1002: return std::make_exception_ptr(ExpiredError(msg)); - case 1003: return std::make_exception_ptr(StoreFullError(msg)); - case 1004: return std::make_exception_ptr(IoError(msg)); - default: return std::make_exception_ptr(KvError(code, msg)); - } -} - -/** Throw the typed `kv` exception if `err` carries a nonzero code. */ -inline void check_kv(weaveffi_error& err) { - if (err.code == 0) return; - std::string msg(err.message ? err.message : "unknown error"); - std::exception_ptr ex = make_kv_error(err.code, msg, err.payload_ptr, err.payload_len); - weaveffi_error_clear(&err); - std::rethrow_exception(ex); -} - -} // namespace detail - -namespace detail { - -inline std::mutex& wv_listener_mutex() { - static std::mutex m; - return m; -} - -inline std::unordered_map>& wv_listener_registry() { - static std::unordered_map> registry; - return registry; -} - -} // namespace detail - -/** An in-memory key-value store owning its entries; destroying the object releases all of its state */ -class Store { - void* handle_; - -public: - explicit Store(void* h) : handle_(h) {} - - ~Store() { - if (handle_) weaveffi_kv_Store_destroy(static_cast(handle_)); - } - - Store(const Store&) = delete; - Store& operator=(const Store&) = delete; - - Store(Store&& other) noexcept : handle_(other.handle_) { - other.handle_ = nullptr; - } - - Store& operator=(Store&& other) noexcept { - if (this != &other) { - if (handle_) weaveffi_kv_Store_destroy(static_cast(handle_)); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - void* handle() const { return handle_; } - - /** Open (or create) a store backed by the given filesystem path */ - static Store open(const std::string& path) { - weaveffi_error err{}; - auto result = weaveffi_kv_Store_open(path.c_str(), &err); - detail::check_kv(err); - return Store(result); - } - - /** Insert or replace a value, returning true on success */ - bool put(const std::string& key, const std::vector& value, EntryKind kind, const std::optional& ttl_seconds) const { - detail::BufferWriter ttl_seconds_buf; - ttl_seconds_buf.write_option_flag(ttl_seconds.has_value()); - if (ttl_seconds.has_value()) { - ttl_seconds_buf.write_i64((*ttl_seconds)); - } - weaveffi_error err{}; - auto result = weaveffi_kv_Store_put(static_cast(handle_), key.c_str(), value.data(), value.size(), static_cast(static_cast(kind)), ttl_seconds_buf.data(), ttl_seconds_buf.size(), &err); - detail::check_kv(err); - return result; - } - - /** Look up an entry by key; returns null if missing or expired */ - std::optional get(const std::string& key) const { - size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_kv_Store_get(static_cast(handle_), key.c_str(), &out_len, &err); - detail::check_kv(err); - detail::BufferGuard result_guard{result, out_len}; - detail::BufferReader result_r(result, out_len); - std::optional ret{}; - if (result_r.read_option_flag()) { - Entry ret_v = detail::read_Entry(result_r); - ret = std::move(ret_v); - } - result_r.expect_end(); - return ret; - } - - /** Remove the entry for the given key, returning true if it existed */ - bool delete_(const std::string& key) const { - weaveffi_error err{}; - auto result = weaveffi_kv_Store_delete(static_cast(handle_), key.c_str(), &err); - detail::check_kv(err); - return result; - } - - /** - * A lazy, move-only range over the `std::string` elements produced by `list_keys()`. - * - * Each iteration step pulls exactly one element from the producer, so results stream in constant memory. The range owns the producer-side iterator and releases it exactly once: eagerly when the range is exhausted, or from the destructor when iteration is abandoned early. - */ - class ListKeysIterator { - weaveffi_kv_Store_ListKeysIterator* handle_; - - public: - /** Adopts ownership of the raw producer iterator handle. */ - explicit ListKeysIterator(weaveffi_kv_Store_ListKeysIterator* h) : handle_(h) {} - - ~ListKeysIterator() { - if (handle_) weaveffi_kv_Store_ListKeysIterator_destroy(handle_); - } - - ListKeysIterator(const ListKeysIterator&) = delete; - ListKeysIterator& operator=(const ListKeysIterator&) = delete; - - ListKeysIterator(ListKeysIterator&& other) noexcept : handle_(other.handle_) { - other.handle_ = nullptr; - } - - ListKeysIterator& operator=(ListKeysIterator&& other) noexcept { - if (this != &other) { - if (handle_) weaveffi_kv_Store_ListKeysIterator_destroy(handle_); - handle_ = other.handle_; - other.handle_ = nullptr; - } - return *this; - } - - /** Pulls the next element from the producer, or `std::nullopt` once exhausted (which releases the producer iterator eagerly). A producer error throws the module's typed exception after releasing the iterator. */ - std::optional next() { - if (!handle_) return std::nullopt; - weaveffi_error err{}; - const char* item{}; - int32_t has_item = weaveffi_kv_Store_ListKeysIterator_next(handle_, &item, &err); - if (err.code != 0) { - weaveffi_kv_Store_ListKeysIterator_destroy(handle_); - handle_ = nullptr; - detail::check_kv(err); - } - if (has_item == 0) { - weaveffi_kv_Store_ListKeysIterator_destroy(handle_); - handle_ = nullptr; - return std::nullopt; - } - std::string value(item); - weaveffi_free_string(item); - return value; - } - - /** Sentinel type marking the end of the range. */ - struct sentinel {}; - - /** Single-pass input iterator; each increment pulls one element from the producer. */ - class iterator { - ListKeysIterator* 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(ListKeysIterator* 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{}; } - }; - - /** - * Stream every key, optionally filtered by a prefix - * - * @return A lazy `ListKeysIterator` range that streams one element per iteration step and releases the producer iterator when exhausted or destroyed. - */ - ListKeysIterator list_keys(const std::optional& prefix) const { - detail::BufferWriter prefix_buf; - prefix_buf.write_option_flag(prefix.has_value()); - if (prefix.has_value()) { - prefix_buf.write_string((*prefix)); - } - weaveffi_error err{}; - weaveffi_kv_Store_ListKeysIterator* iter = weaveffi_kv_Store_list_keys(static_cast(handle_), prefix_buf.data(), prefix_buf.size(), &err); - detail::check_kv(err); - return ListKeysIterator(iter); - } - - /** Return the number of live entries in the store */ - int64_t count() const { - weaveffi_error err{}; - auto result = weaveffi_kv_Store_count(static_cast(handle_), &err); - detail::check(err); - return result; - } - - /** Drop every entry from the store */ - void clear() const { - weaveffi_error err{}; - weaveffi_kv_Store_clear(static_cast(handle_), &err); - detail::check(err); - } - - /** Reclaim space asynchronously; returns the number of bytes reclaimed */ - std::future compact(weaveffi_cancel_token* cancel_token = nullptr) const { - auto* promise_ptr = new std::promise(); - auto future = promise_ptr->get_future(); - weaveffi_kv_Store_compact_async(static_cast(handle_), cancel_token, [](void* context, weaveffi_error* err, int64_t result) { - auto* p = static_cast*>(context); - if (err && err->code != 0) { - std::string msg(err->message ? err->message : "unknown error"); - p->set_exception(detail::make_kv_error(err->code, msg, err->payload_ptr, err->payload_len)); - weaveffi_error_free(err); - } else { - p->set_value(result); - } - delete p; - }, static_cast(promise_ptr)); - return future; - } - - /** Legacy single-shot put kept for compatibility */ - [[deprecated("use put() with explicit kind")]] - bool legacy_put(const std::string& key, const std::vector& value) const { - weaveffi_error err{}; - auto result = weaveffi_kv_Store_legacy_put(static_cast(handle_), key.c_str(), value.data(), value.size(), &err); - detail::check_kv(err); - return result; - } - - /** The largest number of live entries one store will hold */ - static int64_t default_capacity() { - weaveffi_error err{}; - auto result = weaveffi_kv_Store_default_capacity(&err); - detail::check(err); - return result; - } - -}; - -namespace kv { - -/** Subscribe to per-key eviction notifications */ -/** @return A subscription id for unregister_eviction_listener(). */ -inline uint64_t register_eviction_listener(std::function callback) { - auto fn = std::make_shared>(std::move(callback)); - uint64_t id = weaveffi_kv_register_eviction_listener( - [](const char* key, void* context) { - auto& cb = *static_cast*>(context); - cb(std::string(key ? key : "")); - }, - fn.get()); - std::lock_guard lock(detail::wv_listener_mutex()); - detail::wv_listener_registry()[id] = fn; - return id; -} - -/** Unregisters a listener previously registered with register_eviction_listener(). */ -inline void unregister_eviction_listener(uint64_t id) { - weaveffi_kv_unregister_eviction_listener(id); - std::lock_guard lock(detail::wv_listener_mutex()); - detail::wv_listener_registry().erase(id); -} - -} // namespace kv - -namespace kv::stats { - -/** Snapshot the current store statistics */ -inline Stats get_stats(const Store& store) { - size_t out_len = 0; - weaveffi_error err{}; - auto result = weaveffi_kv_stats_get_stats(static_cast(store.handle()), &out_len, &err); - detail::check_kv(err); - detail::BufferGuard result_guard{result, out_len}; - detail::BufferReader result_r(result, out_len); - Stats ret = detail::read_Stats(result_r); - result_r.expect_end(); - return ret; -} - -} // namespace kv::stats - -} // namespace weaveffi - -// END weaveffi.hpp diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__weaveffi_hpp.snap index 4ca70d39..15a42adf 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_nested_modules__weaveffi_hpp.snap @@ -46,6 +46,13 @@ source: crates/weaveffi-cli/tests/snapshots.rs extern "C" { +/* The WeaveFFI C ABI revision this header was generated against. The + producer exports weaveffi_abi_version() so a consumer can refuse to load + a library built for a different revision instead of misreading its + error struct or value buffers. */ +#define WEAVEFFI_ABI_VERSION 1u +WEAVEFFI_API uint32_t weaveffi_abi_version(void); + typedef uint64_t weaveffi_handle_t; /* Error slot written by every fallible call. `payload_ptr`/`payload_len` diff --git a/crates/weaveffi-cli/tests/snapshots/cpp_shapes__weaveffi_hpp.snap b/crates/weaveffi-cli/tests/snapshots/cpp_shapes__weaveffi_hpp.snap index 73a8b2d4..fe78da8b 100644 --- a/crates/weaveffi-cli/tests/snapshots/cpp_shapes__weaveffi_hpp.snap +++ b/crates/weaveffi-cli/tests/snapshots/cpp_shapes__weaveffi_hpp.snap @@ -47,6 +47,13 @@ source: crates/weaveffi-cli/tests/snapshots.rs extern "C" { +/* The WeaveFFI C ABI revision this header was generated against. The + producer exports weaveffi_abi_version() so a consumer can refuse to load + a library built for a different revision instead of misreading its + error struct or value buffers. */ +#define WEAVEFFI_ABI_VERSION 1u +WEAVEFFI_API uint32_t weaveffi_abi_version(void); + typedef uint64_t weaveffi_handle_t; /* Error slot written by every fallible call. `payload_ptr`/`payload_len` diff --git a/crates/weaveffi-cli/tests/snapshots/dart_async_demo__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_async_demo__lib_weaveffi_dart.snap deleted file mode 100644 index d3353042..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_async_demo__lib_weaveffi_dart.snap +++ /dev/null @@ -1,536 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// ignore_for_file: non_constant_identifier_names, camel_case_types, unused_element - -import 'dart:async'; -import 'dart:convert'; -import 'dart:ffi'; -import 'dart:io' show Platform; -import 'dart:typed_data'; - -import 'package:ffi/ffi.dart'; - -DynamicLibrary _openLibrary() { - // An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a - // specific build artifact regardless of its file name or location. - final override = Platform.environment['WEAVEFFI_LIBRARY']; - if (override != null && override.isNotEmpty) return DynamicLibrary.open(override); - if (Platform.isMacOS) return DynamicLibrary.open('libweaveffi.dylib'); - if (Platform.isLinux) return DynamicLibrary.open('libweaveffi.so'); - if (Platform.isWindows) return DynamicLibrary.open('weaveffi.dll'); - throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}'); -} - -final DynamicLibrary _lib = _openLibrary(); - -final class _WeaveFFIError extends Struct { - @Int32() - external int code; - external Pointer message; - external Pointer payloadPtr; - @Size() - external int payloadLen; -} - -typedef _NativeWeaveffiErrorClear = Void Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiErrorClear = void Function(Pointer<_WeaveFFIError>); -final _weaveffiErrorClear = _lib.lookupFunction< - _NativeWeaveffiErrorClear, _DartWeaveffiErrorClear>('weaveffi_error_clear'); - -typedef _NativeWeaveffiErrorFree = Void Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiErrorFree = void Function(Pointer<_WeaveFFIError>); -final _weaveffiErrorFree = _lib.lookupFunction< - _NativeWeaveffiErrorFree, _DartWeaveffiErrorFree>('weaveffi_error_free'); - -typedef _NativeWeaveffiFreeString = Void Function(Pointer); -typedef _DartWeaveffiFreeString = void Function(Pointer); -final _weaveffiFreeString = _lib.lookupFunction< - _NativeWeaveffiFreeString, _DartWeaveffiFreeString>('weaveffi_free_string'); - -typedef _NativeWeaveffiFreeBytes = Void Function(Pointer, Size); -typedef _DartWeaveffiFreeBytes = void Function(Pointer, int); -final _weaveffiFreeBytes = _lib.lookupFunction< - _NativeWeaveffiFreeBytes, _DartWeaveffiFreeBytes>('weaveffi_free_bytes'); - -/// Generic WeaveFFI failure: panics, marshalling errors, and unknown codes. -class WeaveFFIException implements Exception { - final int code; - final String message; - WeaveFFIException(this.code, this.message); - @override - String toString() => '$runtimeType($code): $message'; -} - -void _checkError(Pointer<_WeaveFFIError> err) { - if (err.ref.code != 0) { - final code = err.ref.code; - final msg = err.ref.message.toDartString(); - _weaveffiErrorClear(err); - throw WeaveFFIException(code, msg); - } -} - -// ── WeaveFFI value-buffer runtime ── -// 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 producer/consumer contract violation, never a typed domain error. - -Never _bufferError(String context) => - throw StateError('malformed WeaveFFI value buffer: $context'); - -// Copies a borrowed native (ptr, len) buffer into Dart-owned memory. -Uint8List _copyNativeBytes(Pointer ptr, int len) => - ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); - -// Stages an encoding into native memory for a borrowed (ptr, len) argument; -// the caller frees the pointer after the call returns. -Pointer _stageBytes(Uint8List bytes) { - final ptr = calloc(bytes.isEmpty ? 1 : bytes.length); - if (bytes.isNotEmpty) ptr.asTypedList(bytes.length).setAll(0, bytes); - return ptr; -} - -final class _BufferWriter { - Uint8List _buf = Uint8List(64); - ByteData? _view; - int _len = 0; - - ByteData get _data => _view ??= ByteData.sublistView(_buf); - - void _ensure(int extra) { - if (_len + extra <= _buf.length) return; - var cap = _buf.length * 2; - while (cap < _len + extra) { - cap *= 2; - } - final next = Uint8List(cap); - next.setRange(0, _len, _buf); - _buf = next; - _view = null; - } - - Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); - - void writeBool(bool v) => writeUint8(v ? 1 : 0); - - void writeOptionFlag(bool present) => writeUint8(present ? 1 : 0); - - void writeInt8(int v) { - _ensure(1); - _data.setInt8(_len, v); - _len += 1; - } - - void writeUint8(int v) { - _ensure(1); - _data.setUint8(_len, v); - _len += 1; - } - - void writeInt16(int v) { - _ensure(2); - _data.setInt16(_len, v, Endian.little); - _len += 2; - } - - void writeUint16(int v) { - _ensure(2); - _data.setUint16(_len, v, Endian.little); - _len += 2; - } - - void writeInt32(int v) { - _ensure(4); - _data.setInt32(_len, v, Endian.little); - _len += 4; - } - - void writeUint32(int v) { - _ensure(4); - _data.setUint32(_len, v, Endian.little); - _len += 4; - } - - void writeInt64(int v) { - _ensure(8); - _data.setInt64(_len, v, Endian.little); - _len += 8; - } - - void writeUint64(int v) { - _ensure(8); - _data.setUint64(_len, v, Endian.little); - _len += 8; - } - - void writeFloat32(double v) { - _ensure(4); - _data.setFloat32(_len, v, Endian.little); - _len += 4; - } - - void writeFloat64(double v) { - _ensure(8); - _data.setFloat64(_len, v, Endian.little); - _len += 8; - } - - void writeLength(int v) => writeUint32(v); - - void writeString(String v) { - final bytes = utf8.encode(v); - writeLength(bytes.length); - _ensure(bytes.length); - _buf.setRange(_len, _len + bytes.length, bytes); - _len += bytes.length; - } - - void writeBytes(List v) { - writeLength(v.length); - _ensure(v.length); - _buf.setRange(_len, _len + v.length, v); - _len += v.length; - } -} - -final class _BufferReader { - final Uint8List _buf; - final ByteData _data; - int _pos = 0; - - _BufferReader(Uint8List buf) - : _buf = buf, - _data = ByteData.sublistView(buf); - - int get _remaining => _buf.length - _pos; - - void _require(int n, String context) { - if (_remaining < n) _bufferError(context); - } - - bool readBool() { - _require(1, 'bool'); - final b = _buf[_pos++]; - if (b > 1) _bufferError('bool byte out of range'); - return b == 1; - } - - bool readOptionFlag() { - _require(1, 'option flag'); - final b = _buf[_pos++]; - if (b > 1) _bufferError('option flag byte out of range'); - return b == 1; - } - - int readInt8() { - _require(1, 'i8'); - return _data.getInt8(_pos++); - } - - int readUint8() { - _require(1, 'u8'); - return _buf[_pos++]; - } - - int readInt16() { - _require(2, 'i16'); - final v = _data.getInt16(_pos, Endian.little); - _pos += 2; - return v; - } - - int readUint16() { - _require(2, 'u16'); - final v = _data.getUint16(_pos, Endian.little); - _pos += 2; - return v; - } - - int readInt32() { - _require(4, 'i32'); - final v = _data.getInt32(_pos, Endian.little); - _pos += 4; - return v; - } - - int readUint32() { - _require(4, 'u32'); - final v = _data.getUint32(_pos, Endian.little); - _pos += 4; - return v; - } - - int readInt64() { - _require(8, 'i64'); - final v = _data.getInt64(_pos, Endian.little); - _pos += 8; - return v; - } - - int readUint64() { - _require(8, 'u64'); - final v = _data.getUint64(_pos, Endian.little); - _pos += 8; - return v; - } - - double readFloat32() { - _require(4, 'f32'); - final v = _data.getFloat32(_pos, Endian.little); - _pos += 4; - return v; - } - - double readFloat64() { - _require(8, 'f64'); - final v = _data.getFloat64(_pos, Endian.little); - _pos += 8; - return v; - } - - int readLength() { - final n = readUint32(); - if (n > _remaining) _bufferError('length prefix exceeds remaining buffer'); - return n; - } - - String readString() { - final n = readLength(); - final s = utf8.decode(Uint8List.sublistView(_buf, _pos, _pos + n)); - _pos += n; - return s; - } - - Uint8List readBytes() { - final n = readLength(); - final b = Uint8List.fromList(Uint8List.sublistView(_buf, _pos, _pos + n)); - _pos += n; - return b; - } - - void expectEnd() { - if (_remaining != 0) _bufferError('trailing bytes after value'); - } -} - -/// Typed error domain `TaskError` declared by module `tasks`. -class TaskException extends WeaveFFIException { - TaskException(super.code, super.message); -} - -/// task name must not be empty -class InvalidNameException extends TaskException { - InvalidNameException([String message = 'task name must not be empty']) : super(1, message); -} - -WeaveFFIException _mapTaskException(int code, String message, Uint8List payload) { - switch (code) { - case 1: - return InvalidNameException(message); - default: - return WeaveFFIException(code, message); - } -} - -void _checkTaskException(Pointer<_WeaveFFIError> err) { - if (err.ref.code != 0) { - final code = err.ref.code; - final msg = err.ref.message.toDartString(); - final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen); - _weaveffiErrorClear(err); - throw _mapTaskException(code, msg, payload); - } -} - -class TaskResult { - final int id; - final String value; - final bool success; - - TaskResult({required this.id, required this.value, required this.success}); -} - -void _packTaskResult(_BufferWriter w, TaskResult v) { - w.writeInt64(v.id); - w.writeString(v.value); - w.writeBool(v.success); -} - -TaskResult _unpackTaskResult(_BufferReader r) { - return TaskResult( - id: r.readInt64(), - value: r.readString(), - success: r.readBool(), - ); -} - -typedef _NativeAsyncCb_weaveffi_tasks_run_task = Void Function(Pointer, Pointer<_WeaveFFIError>, Pointer, Size); - -typedef _NativeWeaveffiTasksRunTaskAsync = Void Function(Pointer, Pointer>, Pointer); -typedef _DartWeaveffiTasksRunTaskAsync = void Function(Pointer, Pointer>, Pointer); -final _weaveffiTasksRunTaskAsync = _lib.lookupFunction< - _NativeWeaveffiTasksRunTaskAsync, _DartWeaveffiTasksRunTaskAsync>('weaveffi_tasks_run_task_async'); - -/// Throws [TaskException] on domain errors. -Future runTask(String name) { - final completer = Completer(); - final namePtr = name.toNativeUtf8(); - late NativeCallable<_NativeAsyncCb_weaveffi_tasks_run_task> callable; - callable = NativeCallable<_NativeAsyncCb_weaveffi_tasks_run_task>.listener((Pointer context, Pointer<_WeaveFFIError> err, Pointer result, int resultLen) { - try { - if (err.address != 0 && err.ref.code != 0) { - final code = err.ref.code; - final msg = err.ref.message.toDartString(); - final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen); - _weaveffiErrorFree(err); - completer.completeError(_mapTaskException(code, msg, payload)); - return; - } - final resultData = _copyNativeBytes(result, resultLen); - _weaveffiFreeBytes(result, resultLen); - final resultReader = _BufferReader(resultData); - final value = _unpackTaskResult(resultReader); - resultReader.expectEnd(); - completer.complete(value); - } catch (e) { - completer.completeError(e); - } finally { - callable.close(); - } - }); - try { - _weaveffiTasksRunTaskAsync(namePtr, callable.nativeFunction, nullptr); - } catch (e) { - callable.close(); - calloc.free(namePtr); - rethrow; - } - return completer.future.whenComplete(() { - calloc.free(namePtr); - }); -} - -typedef _NativeAsyncCb_weaveffi_tasks_run_batch = Void Function(Pointer, Pointer<_WeaveFFIError>, Pointer, Size); - -typedef _NativeWeaveffiTasksRunBatchAsync = Void Function(Pointer, Size, Pointer>, Pointer); -typedef _DartWeaveffiTasksRunBatchAsync = void Function(Pointer, int, Pointer>, Pointer); -final _weaveffiTasksRunBatchAsync = _lib.lookupFunction< - _NativeWeaveffiTasksRunBatchAsync, _DartWeaveffiTasksRunBatchAsync>('weaveffi_tasks_run_batch_async'); - -Future> runBatch(List names) { - final completer = Completer>(); - final namesWriter = _BufferWriter(); - final t0 = names; - namesWriter.writeLength(t0.length); - for (final t1 in t0) { - namesWriter.writeString(t1); - } - final namesBuf = namesWriter.takeBytes(); - final namesPtr = _stageBytes(namesBuf); - late NativeCallable<_NativeAsyncCb_weaveffi_tasks_run_batch> callable; - callable = NativeCallable<_NativeAsyncCb_weaveffi_tasks_run_batch>.listener((Pointer context, Pointer<_WeaveFFIError> err, Pointer result, int resultLen) { - try { - if (err.address != 0 && err.ref.code != 0) { - final code = err.ref.code; - final msg = err.ref.message.toDartString(); - _weaveffiErrorFree(err); - completer.completeError(WeaveFFIException(code, msg)); - return; - } - final resultData = _copyNativeBytes(result, resultLen); - _weaveffiFreeBytes(result, resultLen); - final resultReader = _BufferReader(resultData); - final value = List.generate(resultReader.readLength(), (_) => _unpackTaskResult(resultReader)); - resultReader.expectEnd(); - completer.complete(value); - } catch (e) { - completer.completeError(e); - } finally { - callable.close(); - } - }); - try { - _weaveffiTasksRunBatchAsync(namesPtr, namesBuf.length, callable.nativeFunction, nullptr); - } catch (e) { - callable.close(); - calloc.free(namesPtr); - rethrow; - } - return completer.future.whenComplete(() { - calloc.free(namesPtr); - }); -} - -typedef _NativeWeaveffiTasksCancelTask = Bool Function(Int64, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiTasksCancelTask = bool Function(int, Pointer<_WeaveFFIError>); -final _weaveffiTasksCancelTask = _lib.lookupFunction< - _NativeWeaveffiTasksCancelTask, _DartWeaveffiTasksCancelTask>('weaveffi_tasks_cancel_task'); - -bool cancelTask(int id) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiTasksCancelTask(id, err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } -} - -typedef _NativeAsyncCb_weaveffi_tasks_run_n_tasks = Void Function(Pointer, Pointer<_WeaveFFIError>, Int32); - -typedef _NativeWeaveffiTasksRunNTasksAsync = Void Function(Int32, Pointer>, Pointer); -typedef _DartWeaveffiTasksRunNTasksAsync = void Function(int, Pointer>, Pointer); -final _weaveffiTasksRunNTasksAsync = _lib.lookupFunction< - _NativeWeaveffiTasksRunNTasksAsync, _DartWeaveffiTasksRunNTasksAsync>('weaveffi_tasks_run_n_tasks_async'); - -Future runNTasks(int n) { - final completer = Completer(); - late NativeCallable<_NativeAsyncCb_weaveffi_tasks_run_n_tasks> callable; - callable = NativeCallable<_NativeAsyncCb_weaveffi_tasks_run_n_tasks>.listener((Pointer context, Pointer<_WeaveFFIError> err, int result) { - try { - if (err.address != 0 && err.ref.code != 0) { - final code = err.ref.code; - final msg = err.ref.message.toDartString(); - _weaveffiErrorFree(err); - completer.completeError(WeaveFFIException(code, msg)); - return; - } - completer.complete(result); - } catch (e) { - completer.completeError(e); - } finally { - callable.close(); - } - }); - try { - _weaveffiTasksRunNTasksAsync(n, callable.nativeFunction, nullptr); - } catch (e) { - callable.close(); - rethrow; - } - return completer.future; -} - -typedef _NativeWeaveffiTasksActiveCallbacks = Int64 Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiTasksActiveCallbacks = int Function(Pointer<_WeaveFFIError>); -final _weaveffiTasksActiveCallbacks = _lib.lookupFunction< - _NativeWeaveffiTasksActiveCallbacks, _DartWeaveffiTasksActiveCallbacks>('weaveffi_tasks_active_callbacks'); - -int activeCallbacks() { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiTasksActiveCallbacks(err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } -} - -// END weaveffi.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_calculator__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dart_calculator__README_md.snap deleted file mode 100644 index daa40993..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_calculator__README_md.snap +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# weaveffi (Dart) - -Auto-generated Dart bindings using `dart:ffi`. - -## Usage - -1. Place the compiled shared library (`libweaveffi.dylib`, `libweaveffi.so`, - or `weaveffi.dll`) where the Dart process can find it. - -2. Add this package as a dependency and import the bindings: - -```dart -import 'package:weaveffi/weaveffi.dart'; -``` - -3. Call the generated functions directly. The bindings use `dart:ffi` to load - the native library at runtime via `DynamicLibrary.open` and resolve symbols - with `lookupFunction`. - -## Requirements - -- Dart SDK >= 3.0.0 -- The `ffi` package (`^2.0.0`) for `Utf8` and `calloc` helpers. - - diff --git a/crates/weaveffi-cli/tests/snapshots/dart_calculator__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_calculator__lib_weaveffi_dart.snap deleted file mode 100644 index 5076c9bd..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_calculator__lib_weaveffi_dart.snap +++ /dev/null @@ -1,423 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// ignore_for_file: non_constant_identifier_names, camel_case_types, unused_element - -import 'dart:convert'; -import 'dart:ffi'; -import 'dart:io' show Platform; -import 'dart:typed_data'; - -import 'package:ffi/ffi.dart'; - -DynamicLibrary _openLibrary() { - // An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a - // specific build artifact regardless of its file name or location. - final override = Platform.environment['WEAVEFFI_LIBRARY']; - if (override != null && override.isNotEmpty) return DynamicLibrary.open(override); - if (Platform.isMacOS) return DynamicLibrary.open('libweaveffi.dylib'); - if (Platform.isLinux) return DynamicLibrary.open('libweaveffi.so'); - if (Platform.isWindows) return DynamicLibrary.open('weaveffi.dll'); - throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}'); -} - -final DynamicLibrary _lib = _openLibrary(); - -final class _WeaveFFIError extends Struct { - @Int32() - external int code; - external Pointer message; - external Pointer payloadPtr; - @Size() - external int payloadLen; -} - -typedef _NativeWeaveffiErrorClear = Void Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiErrorClear = void Function(Pointer<_WeaveFFIError>); -final _weaveffiErrorClear = _lib.lookupFunction< - _NativeWeaveffiErrorClear, _DartWeaveffiErrorClear>('weaveffi_error_clear'); - -typedef _NativeWeaveffiErrorFree = Void Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiErrorFree = void Function(Pointer<_WeaveFFIError>); -final _weaveffiErrorFree = _lib.lookupFunction< - _NativeWeaveffiErrorFree, _DartWeaveffiErrorFree>('weaveffi_error_free'); - -typedef _NativeWeaveffiFreeString = Void Function(Pointer); -typedef _DartWeaveffiFreeString = void Function(Pointer); -final _weaveffiFreeString = _lib.lookupFunction< - _NativeWeaveffiFreeString, _DartWeaveffiFreeString>('weaveffi_free_string'); - -typedef _NativeWeaveffiFreeBytes = Void Function(Pointer, Size); -typedef _DartWeaveffiFreeBytes = void Function(Pointer, int); -final _weaveffiFreeBytes = _lib.lookupFunction< - _NativeWeaveffiFreeBytes, _DartWeaveffiFreeBytes>('weaveffi_free_bytes'); - -/// Generic WeaveFFI failure: panics, marshalling errors, and unknown codes. -class WeaveFFIException implements Exception { - final int code; - final String message; - WeaveFFIException(this.code, this.message); - @override - String toString() => '$runtimeType($code): $message'; -} - -void _checkError(Pointer<_WeaveFFIError> err) { - if (err.ref.code != 0) { - final code = err.ref.code; - final msg = err.ref.message.toDartString(); - _weaveffiErrorClear(err); - throw WeaveFFIException(code, msg); - } -} - -// ── WeaveFFI value-buffer runtime ── -// 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 producer/consumer contract violation, never a typed domain error. - -Never _bufferError(String context) => - throw StateError('malformed WeaveFFI value buffer: $context'); - -// Copies a borrowed native (ptr, len) buffer into Dart-owned memory. -Uint8List _copyNativeBytes(Pointer ptr, int len) => - ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); - -// Stages an encoding into native memory for a borrowed (ptr, len) argument; -// the caller frees the pointer after the call returns. -Pointer _stageBytes(Uint8List bytes) { - final ptr = calloc(bytes.isEmpty ? 1 : bytes.length); - if (bytes.isNotEmpty) ptr.asTypedList(bytes.length).setAll(0, bytes); - return ptr; -} - -final class _BufferWriter { - Uint8List _buf = Uint8List(64); - ByteData? _view; - int _len = 0; - - ByteData get _data => _view ??= ByteData.sublistView(_buf); - - void _ensure(int extra) { - if (_len + extra <= _buf.length) return; - var cap = _buf.length * 2; - while (cap < _len + extra) { - cap *= 2; - } - final next = Uint8List(cap); - next.setRange(0, _len, _buf); - _buf = next; - _view = null; - } - - Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); - - void writeBool(bool v) => writeUint8(v ? 1 : 0); - - void writeOptionFlag(bool present) => writeUint8(present ? 1 : 0); - - void writeInt8(int v) { - _ensure(1); - _data.setInt8(_len, v); - _len += 1; - } - - void writeUint8(int v) { - _ensure(1); - _data.setUint8(_len, v); - _len += 1; - } - - void writeInt16(int v) { - _ensure(2); - _data.setInt16(_len, v, Endian.little); - _len += 2; - } - - void writeUint16(int v) { - _ensure(2); - _data.setUint16(_len, v, Endian.little); - _len += 2; - } - - void writeInt32(int v) { - _ensure(4); - _data.setInt32(_len, v, Endian.little); - _len += 4; - } - - void writeUint32(int v) { - _ensure(4); - _data.setUint32(_len, v, Endian.little); - _len += 4; - } - - void writeInt64(int v) { - _ensure(8); - _data.setInt64(_len, v, Endian.little); - _len += 8; - } - - void writeUint64(int v) { - _ensure(8); - _data.setUint64(_len, v, Endian.little); - _len += 8; - } - - void writeFloat32(double v) { - _ensure(4); - _data.setFloat32(_len, v, Endian.little); - _len += 4; - } - - void writeFloat64(double v) { - _ensure(8); - _data.setFloat64(_len, v, Endian.little); - _len += 8; - } - - void writeLength(int v) => writeUint32(v); - - void writeString(String v) { - final bytes = utf8.encode(v); - writeLength(bytes.length); - _ensure(bytes.length); - _buf.setRange(_len, _len + bytes.length, bytes); - _len += bytes.length; - } - - void writeBytes(List v) { - writeLength(v.length); - _ensure(v.length); - _buf.setRange(_len, _len + v.length, v); - _len += v.length; - } -} - -final class _BufferReader { - final Uint8List _buf; - final ByteData _data; - int _pos = 0; - - _BufferReader(Uint8List buf) - : _buf = buf, - _data = ByteData.sublistView(buf); - - int get _remaining => _buf.length - _pos; - - void _require(int n, String context) { - if (_remaining < n) _bufferError(context); - } - - bool readBool() { - _require(1, 'bool'); - final b = _buf[_pos++]; - if (b > 1) _bufferError('bool byte out of range'); - return b == 1; - } - - bool readOptionFlag() { - _require(1, 'option flag'); - final b = _buf[_pos++]; - if (b > 1) _bufferError('option flag byte out of range'); - return b == 1; - } - - int readInt8() { - _require(1, 'i8'); - return _data.getInt8(_pos++); - } - - int readUint8() { - _require(1, 'u8'); - return _buf[_pos++]; - } - - int readInt16() { - _require(2, 'i16'); - final v = _data.getInt16(_pos, Endian.little); - _pos += 2; - return v; - } - - int readUint16() { - _require(2, 'u16'); - final v = _data.getUint16(_pos, Endian.little); - _pos += 2; - return v; - } - - int readInt32() { - _require(4, 'i32'); - final v = _data.getInt32(_pos, Endian.little); - _pos += 4; - return v; - } - - int readUint32() { - _require(4, 'u32'); - final v = _data.getUint32(_pos, Endian.little); - _pos += 4; - return v; - } - - int readInt64() { - _require(8, 'i64'); - final v = _data.getInt64(_pos, Endian.little); - _pos += 8; - return v; - } - - int readUint64() { - _require(8, 'u64'); - final v = _data.getUint64(_pos, Endian.little); - _pos += 8; - return v; - } - - double readFloat32() { - _require(4, 'f32'); - final v = _data.getFloat32(_pos, Endian.little); - _pos += 4; - return v; - } - - double readFloat64() { - _require(8, 'f64'); - final v = _data.getFloat64(_pos, Endian.little); - _pos += 8; - return v; - } - - int readLength() { - final n = readUint32(); - if (n > _remaining) _bufferError('length prefix exceeds remaining buffer'); - return n; - } - - String readString() { - final n = readLength(); - final s = utf8.decode(Uint8List.sublistView(_buf, _pos, _pos + n)); - _pos += n; - return s; - } - - Uint8List readBytes() { - final n = readLength(); - final b = Uint8List.fromList(Uint8List.sublistView(_buf, _pos, _pos + n)); - _pos += n; - return b; - } - - void expectEnd() { - if (_remaining != 0) _bufferError('trailing bytes after value'); - } -} - -/// Typed error domain `CalcError` declared by module `calculator`. -class CalcException extends WeaveFFIException { - CalcException(super.code, super.message); -} - -/// division by zero -class DivisionByZeroException extends CalcException { - DivisionByZeroException([String message = 'division by zero']) : super(1, message); -} - -WeaveFFIException _mapCalcException(int code, String message, Uint8List payload) { - switch (code) { - case 1: - return DivisionByZeroException(message); - default: - return WeaveFFIException(code, message); - } -} - -void _checkCalcException(Pointer<_WeaveFFIError> err) { - if (err.ref.code != 0) { - final code = err.ref.code; - final msg = err.ref.message.toDartString(); - final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen); - _weaveffiErrorClear(err); - throw _mapCalcException(code, msg, payload); - } -} - -typedef _NativeWeaveffiCalculatorAdd = Int32 Function(Int32, Int32, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiCalculatorAdd = int Function(int, int, Pointer<_WeaveFFIError>); -final _weaveffiCalculatorAdd = _lib.lookupFunction< - _NativeWeaveffiCalculatorAdd, _DartWeaveffiCalculatorAdd>('weaveffi_calculator_add'); - -/// Add two integers -int add(int a, int b) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiCalculatorAdd(a, b, err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } -} - -typedef _NativeWeaveffiCalculatorMul = Int32 Function(Int32, Int32, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiCalculatorMul = int Function(int, int, Pointer<_WeaveFFIError>); -final _weaveffiCalculatorMul = _lib.lookupFunction< - _NativeWeaveffiCalculatorMul, _DartWeaveffiCalculatorMul>('weaveffi_calculator_mul'); - -/// Multiply two integers -int mul(int a, int b) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiCalculatorMul(a, b, err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } -} - -typedef _NativeWeaveffiCalculatorDiv = Int32 Function(Int32, Int32, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiCalculatorDiv = int Function(int, int, Pointer<_WeaveFFIError>); -final _weaveffiCalculatorDiv = _lib.lookupFunction< - _NativeWeaveffiCalculatorDiv, _DartWeaveffiCalculatorDiv>('weaveffi_calculator_div'); - -/// Divide two integers, failing on a zero divisor -/// -/// Throws [CalcException] on domain errors. -int div(int a, int b) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiCalculatorDiv(a, b, err); - _checkCalcException(err); - return result; - } finally { - calloc.free(err); - } -} - -typedef _NativeWeaveffiCalculatorEcho = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiCalculatorEcho = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -final _weaveffiCalculatorEcho = _lib.lookupFunction< - _NativeWeaveffiCalculatorEcho, _DartWeaveffiCalculatorEcho>('weaveffi_calculator_echo'); - -/// Echo a string back -String echo(String s) { - final sPtr = s.toNativeUtf8(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiCalculatorEcho(sPtr, err); - _checkError(err); - final value = result.toDartString(); - _weaveffiFreeString(result); - return value; - } finally { - calloc.free(sPtr); - calloc.free(err); - } -} - -// END weaveffi.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_calculator__pubspec_yaml.snap b/crates/weaveffi-cli/tests/snapshots/dart_calculator__pubspec_yaml.snap deleted file mode 100644 index 6c4a1026..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_calculator__pubspec_yaml.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -name: weaveffi -version: 0.1.0 -description: weaveffi bindings generated by WeaveFFI -environment: - sdk: '>=3.0.0 <4.0.0' -dependencies: - ffi: ^2.0.0 - -# END pubspec.yaml diff --git a/crates/weaveffi-cli/tests/snapshots/dart_contacts__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dart_contacts__README_md.snap deleted file mode 100644 index daa40993..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_contacts__README_md.snap +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# weaveffi (Dart) - -Auto-generated Dart bindings using `dart:ffi`. - -## Usage - -1. Place the compiled shared library (`libweaveffi.dylib`, `libweaveffi.so`, - or `weaveffi.dll`) where the Dart process can find it. - -2. Add this package as a dependency and import the bindings: - -```dart -import 'package:weaveffi/weaveffi.dart'; -``` - -3. Call the generated functions directly. The bindings use `dart:ffi` to load - the native library at runtime via `DynamicLibrary.open` and resolve symbols - with `lookupFunction`. - -## Requirements - -- Dart SDK >= 3.0.0 -- The `ffi` package (`^2.0.0`) for `Utf8` and `calloc` helpers. - - diff --git a/crates/weaveffi-cli/tests/snapshots/dart_contacts__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_contacts__lib_weaveffi_dart.snap deleted file mode 100644 index b749a69f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_contacts__lib_weaveffi_dart.snap +++ /dev/null @@ -1,555 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// ignore_for_file: non_constant_identifier_names, camel_case_types, unused_element - -import 'dart:convert'; -import 'dart:ffi'; -import 'dart:io' show Platform; -import 'dart:typed_data'; - -import 'package:ffi/ffi.dart'; - -DynamicLibrary _openLibrary() { - // An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a - // specific build artifact regardless of its file name or location. - final override = Platform.environment['WEAVEFFI_LIBRARY']; - if (override != null && override.isNotEmpty) return DynamicLibrary.open(override); - if (Platform.isMacOS) return DynamicLibrary.open('libweaveffi.dylib'); - if (Platform.isLinux) return DynamicLibrary.open('libweaveffi.so'); - if (Platform.isWindows) return DynamicLibrary.open('weaveffi.dll'); - throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}'); -} - -final DynamicLibrary _lib = _openLibrary(); - -final class _WeaveFFIError extends Struct { - @Int32() - external int code; - external Pointer message; - external Pointer payloadPtr; - @Size() - external int payloadLen; -} - -typedef _NativeWeaveffiErrorClear = Void Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiErrorClear = void Function(Pointer<_WeaveFFIError>); -final _weaveffiErrorClear = _lib.lookupFunction< - _NativeWeaveffiErrorClear, _DartWeaveffiErrorClear>('weaveffi_error_clear'); - -typedef _NativeWeaveffiErrorFree = Void Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiErrorFree = void Function(Pointer<_WeaveFFIError>); -final _weaveffiErrorFree = _lib.lookupFunction< - _NativeWeaveffiErrorFree, _DartWeaveffiErrorFree>('weaveffi_error_free'); - -typedef _NativeWeaveffiFreeString = Void Function(Pointer); -typedef _DartWeaveffiFreeString = void Function(Pointer); -final _weaveffiFreeString = _lib.lookupFunction< - _NativeWeaveffiFreeString, _DartWeaveffiFreeString>('weaveffi_free_string'); - -typedef _NativeWeaveffiFreeBytes = Void Function(Pointer, Size); -typedef _DartWeaveffiFreeBytes = void Function(Pointer, int); -final _weaveffiFreeBytes = _lib.lookupFunction< - _NativeWeaveffiFreeBytes, _DartWeaveffiFreeBytes>('weaveffi_free_bytes'); - -/// Generic WeaveFFI failure: panics, marshalling errors, and unknown codes. -class WeaveFFIException implements Exception { - final int code; - final String message; - WeaveFFIException(this.code, this.message); - @override - String toString() => '$runtimeType($code): $message'; -} - -void _checkError(Pointer<_WeaveFFIError> err) { - if (err.ref.code != 0) { - final code = err.ref.code; - final msg = err.ref.message.toDartString(); - _weaveffiErrorClear(err); - throw WeaveFFIException(code, msg); - } -} - -// ── WeaveFFI value-buffer runtime ── -// 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 producer/consumer contract violation, never a typed domain error. - -Never _bufferError(String context) => - throw StateError('malformed WeaveFFI value buffer: $context'); - -// Copies a borrowed native (ptr, len) buffer into Dart-owned memory. -Uint8List _copyNativeBytes(Pointer ptr, int len) => - ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); - -// Stages an encoding into native memory for a borrowed (ptr, len) argument; -// the caller frees the pointer after the call returns. -Pointer _stageBytes(Uint8List bytes) { - final ptr = calloc(bytes.isEmpty ? 1 : bytes.length); - if (bytes.isNotEmpty) ptr.asTypedList(bytes.length).setAll(0, bytes); - return ptr; -} - -final class _BufferWriter { - Uint8List _buf = Uint8List(64); - ByteData? _view; - int _len = 0; - - ByteData get _data => _view ??= ByteData.sublistView(_buf); - - void _ensure(int extra) { - if (_len + extra <= _buf.length) return; - var cap = _buf.length * 2; - while (cap < _len + extra) { - cap *= 2; - } - final next = Uint8List(cap); - next.setRange(0, _len, _buf); - _buf = next; - _view = null; - } - - Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); - - void writeBool(bool v) => writeUint8(v ? 1 : 0); - - void writeOptionFlag(bool present) => writeUint8(present ? 1 : 0); - - void writeInt8(int v) { - _ensure(1); - _data.setInt8(_len, v); - _len += 1; - } - - void writeUint8(int v) { - _ensure(1); - _data.setUint8(_len, v); - _len += 1; - } - - void writeInt16(int v) { - _ensure(2); - _data.setInt16(_len, v, Endian.little); - _len += 2; - } - - void writeUint16(int v) { - _ensure(2); - _data.setUint16(_len, v, Endian.little); - _len += 2; - } - - void writeInt32(int v) { - _ensure(4); - _data.setInt32(_len, v, Endian.little); - _len += 4; - } - - void writeUint32(int v) { - _ensure(4); - _data.setUint32(_len, v, Endian.little); - _len += 4; - } - - void writeInt64(int v) { - _ensure(8); - _data.setInt64(_len, v, Endian.little); - _len += 8; - } - - void writeUint64(int v) { - _ensure(8); - _data.setUint64(_len, v, Endian.little); - _len += 8; - } - - void writeFloat32(double v) { - _ensure(4); - _data.setFloat32(_len, v, Endian.little); - _len += 4; - } - - void writeFloat64(double v) { - _ensure(8); - _data.setFloat64(_len, v, Endian.little); - _len += 8; - } - - void writeLength(int v) => writeUint32(v); - - void writeString(String v) { - final bytes = utf8.encode(v); - writeLength(bytes.length); - _ensure(bytes.length); - _buf.setRange(_len, _len + bytes.length, bytes); - _len += bytes.length; - } - - void writeBytes(List v) { - writeLength(v.length); - _ensure(v.length); - _buf.setRange(_len, _len + v.length, v); - _len += v.length; - } -} - -final class _BufferReader { - final Uint8List _buf; - final ByteData _data; - int _pos = 0; - - _BufferReader(Uint8List buf) - : _buf = buf, - _data = ByteData.sublistView(buf); - - int get _remaining => _buf.length - _pos; - - void _require(int n, String context) { - if (_remaining < n) _bufferError(context); - } - - bool readBool() { - _require(1, 'bool'); - final b = _buf[_pos++]; - if (b > 1) _bufferError('bool byte out of range'); - return b == 1; - } - - bool readOptionFlag() { - _require(1, 'option flag'); - final b = _buf[_pos++]; - if (b > 1) _bufferError('option flag byte out of range'); - return b == 1; - } - - int readInt8() { - _require(1, 'i8'); - return _data.getInt8(_pos++); - } - - int readUint8() { - _require(1, 'u8'); - return _buf[_pos++]; - } - - int readInt16() { - _require(2, 'i16'); - final v = _data.getInt16(_pos, Endian.little); - _pos += 2; - return v; - } - - int readUint16() { - _require(2, 'u16'); - final v = _data.getUint16(_pos, Endian.little); - _pos += 2; - return v; - } - - int readInt32() { - _require(4, 'i32'); - final v = _data.getInt32(_pos, Endian.little); - _pos += 4; - return v; - } - - int readUint32() { - _require(4, 'u32'); - final v = _data.getUint32(_pos, Endian.little); - _pos += 4; - return v; - } - - int readInt64() { - _require(8, 'i64'); - final v = _data.getInt64(_pos, Endian.little); - _pos += 8; - return v; - } - - int readUint64() { - _require(8, 'u64'); - final v = _data.getUint64(_pos, Endian.little); - _pos += 8; - return v; - } - - double readFloat32() { - _require(4, 'f32'); - final v = _data.getFloat32(_pos, Endian.little); - _pos += 4; - return v; - } - - double readFloat64() { - _require(8, 'f64'); - final v = _data.getFloat64(_pos, Endian.little); - _pos += 8; - return v; - } - - int readLength() { - final n = readUint32(); - if (n > _remaining) _bufferError('length prefix exceeds remaining buffer'); - return n; - } - - String readString() { - final n = readLength(); - final s = utf8.decode(Uint8List.sublistView(_buf, _pos, _pos + n)); - _pos += n; - return s; - } - - Uint8List readBytes() { - final n = readLength(); - final b = Uint8List.fromList(Uint8List.sublistView(_buf, _pos, _pos + n)); - _pos += n; - return b; - } - - void expectEnd() { - if (_remaining != 0) _bufferError('trailing bytes after value'); - } -} - -/// Typed error domain `ContactsError` declared by module `contacts`. -class ContactsException extends WeaveFFIException { - ContactsException(super.code, super.message); -} - -/// name must not be empty -class InvalidNameException extends ContactsException { - InvalidNameException([String message = 'name must not be empty']) : super(1, message); -} - -/// contact not found -class NotFoundException extends ContactsException { - NotFoundException([String message = 'contact not found']) : super(2, message); -} - -WeaveFFIException _mapContactsException(int code, String message, Uint8List payload) { - switch (code) { - case 1: - return InvalidNameException(message); - case 2: - return NotFoundException(message); - default: - return WeaveFFIException(code, message); - } -} - -void _checkContactsException(Pointer<_WeaveFFIError> err) { - if (err.ref.code != 0) { - final code = err.ref.code; - final msg = err.ref.message.toDartString(); - final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen); - _weaveffiErrorClear(err); - throw _mapContactsException(code, msg, payload); - } -} - -enum ContactType { - personal(0), - work(1), - other(2), - ; - const ContactType(this.value); - final int value; - - static ContactType fromValue(int value) => - ContactType.values.firstWhere((e) => e.value == value); -} - -class Contact { - final int id; - final String firstName; - final String lastName; - final String? email; - final ContactType contactType; - - Contact({required this.id, required this.firstName, required this.lastName, this.email, required this.contactType}); -} - -void _packContact(_BufferWriter w, Contact v) { - w.writeInt64(v.id); - w.writeString(v.firstName); - w.writeString(v.lastName); - final t0 = v.email; - if (t0 == null) { - w.writeOptionFlag(false); - } else { - w.writeOptionFlag(true); - w.writeString(t0); - } - w.writeInt32(v.contactType.value); -} - -Contact _unpackContact(_BufferReader r) { - return Contact( - id: r.readInt64(), - firstName: r.readString(), - lastName: r.readString(), - email: (r.readOptionFlag() ? r.readString() : null), - contactType: ContactType.fromValue(r.readInt32()), - ); -} - -typedef _NativeWeaveffiContactsContactBookDestroy = Void Function(Pointer); -typedef _DartWeaveffiContactsContactBookDestroy = void Function(Pointer); -final _weaveffiContactsContactBookDestroy = _lib.lookupFunction< - _NativeWeaveffiContactsContactBookDestroy, _DartWeaveffiContactsContactBookDestroy>('weaveffi_contacts_ContactBook_destroy'); - -typedef _NativeWeaveffiContactsContactBookNew = Pointer Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiContactsContactBookNew = Pointer Function(Pointer<_WeaveFFIError>); -final _weaveffiContactsContactBookNew = _lib.lookupFunction< - _NativeWeaveffiContactsContactBookNew, _DartWeaveffiContactsContactBookNew>('weaveffi_contacts_ContactBook_new'); - -typedef _NativeWeaveffiContactsContactBookAdd = Pointer Function(Pointer, Pointer, Pointer, Pointer, Size, Int32, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiContactsContactBookAdd = Pointer Function(Pointer, Pointer, Pointer, Pointer, int, int, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiContactsContactBookAdd = _lib.lookupFunction< - _NativeWeaveffiContactsContactBookAdd, _DartWeaveffiContactsContactBookAdd>('weaveffi_contacts_ContactBook_add'); - -typedef _NativeWeaveffiContactsContactBookGet = Pointer Function(Pointer, Int64, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiContactsContactBookGet = Pointer Function(Pointer, int, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiContactsContactBookGet = _lib.lookupFunction< - _NativeWeaveffiContactsContactBookGet, _DartWeaveffiContactsContactBookGet>('weaveffi_contacts_ContactBook_get'); - -typedef _NativeWeaveffiContactsContactBookList = Pointer Function(Pointer, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiContactsContactBookList = Pointer Function(Pointer, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiContactsContactBookList = _lib.lookupFunction< - _NativeWeaveffiContactsContactBookList, _DartWeaveffiContactsContactBookList>('weaveffi_contacts_ContactBook_list'); - -typedef _NativeWeaveffiContactsContactBookRemove = Bool Function(Pointer, Int64, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiContactsContactBookRemove = bool Function(Pointer, int, Pointer<_WeaveFFIError>); -final _weaveffiContactsContactBookRemove = _lib.lookupFunction< - _NativeWeaveffiContactsContactBookRemove, _DartWeaveffiContactsContactBookRemove>('weaveffi_contacts_ContactBook_remove'); - -typedef _NativeWeaveffiContactsContactBookCount = Int32 Function(Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiContactsContactBookCount = int Function(Pointer, Pointer<_WeaveFFIError>); -final _weaveffiContactsContactBookCount = _lib.lookupFunction< - _NativeWeaveffiContactsContactBookCount, _DartWeaveffiContactsContactBookCount>('weaveffi_contacts_ContactBook_count'); - -/// An in-memory address book owning its contacts -class ContactBook { - final Pointer _handle; - ContactBook._(this._handle); - - /// Releases the native object reference. - void dispose() { - _weaveffiContactsContactBookDestroy(_handle); - } - - factory ContactBook() { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiContactsContactBookNew(err); - _checkError(err); - return ContactBook._(result); - } finally { - calloc.free(err); - } - } - - /// Throws [ContactsException] on domain errors. - Contact add(String firstName, String lastName, String? email, ContactType contactType) { - final firstNamePtr = firstName.toNativeUtf8(); - final lastNamePtr = lastName.toNativeUtf8(); - final emailWriter = _BufferWriter(); - final t0 = email; - if (t0 == null) { - emailWriter.writeOptionFlag(false); - } else { - emailWriter.writeOptionFlag(true); - emailWriter.writeString(t0); - } - final emailBuf = emailWriter.takeBytes(); - final emailPtr = _stageBytes(emailBuf); - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiContactsContactBookAdd(_handle, firstNamePtr, lastNamePtr, emailPtr, emailBuf.length, contactType.value, outLen, err); - _checkContactsException(err); - final n = outLen.value; - final data = _copyNativeBytes(result, n); - if (result != nullptr) _weaveffiFreeBytes(result, n); - final reader = _BufferReader(data); - final value = _unpackContact(reader); - reader.expectEnd(); - return value; - } finally { - calloc.free(firstNamePtr); - calloc.free(lastNamePtr); - calloc.free(emailPtr); - calloc.free(outLen); - calloc.free(err); - } - } - - /// Throws [ContactsException] on domain errors. - Contact get(int id) { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiContactsContactBookGet(_handle, id, outLen, err); - _checkContactsException(err); - final n = outLen.value; - final data = _copyNativeBytes(result, n); - if (result != nullptr) _weaveffiFreeBytes(result, n); - final reader = _BufferReader(data); - final value = _unpackContact(reader); - reader.expectEnd(); - return value; - } finally { - calloc.free(outLen); - calloc.free(err); - } - } - - List list() { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiContactsContactBookList(_handle, outLen, err); - _checkError(err); - final n = outLen.value; - final data = _copyNativeBytes(result, n); - if (result != nullptr) _weaveffiFreeBytes(result, n); - final reader = _BufferReader(data); - final value = List.generate(reader.readLength(), (_) => _unpackContact(reader)); - reader.expectEnd(); - return value; - } finally { - calloc.free(outLen); - calloc.free(err); - } - } - - bool remove(int id) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiContactsContactBookRemove(_handle, id, err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } - } - - int count() { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiContactsContactBookCount(_handle, err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } - } -} - -// END weaveffi.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_contacts__pubspec_yaml.snap b/crates/weaveffi-cli/tests/snapshots/dart_contacts__pubspec_yaml.snap deleted file mode 100644 index 6c4a1026..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_contacts__pubspec_yaml.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -name: weaveffi -version: 0.1.0 -description: weaveffi bindings generated by WeaveFFI -environment: - sdk: '>=3.0.0 <4.0.0' -dependencies: - ffi: ^2.0.0 - -# END pubspec.yaml diff --git a/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_weaveffi_dart.snap index 382bab49..bd975f26 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_weaveffi_dart.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_docs_everywhere__lib_weaveffi_dart.snap @@ -26,7 +26,30 @@ DynamicLibrary _openLibrary() { throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}'); } -final DynamicLibrary _lib = _openLibrary(); +// The ABI revision these bindings were generated against. +const int _abiVersion = 1; + +DynamicLibrary _checkAbiVersion(DynamicLibrary lib) { + final int Function() abiVersion; + try { + abiVersion = lib.lookupFunction('weaveffi_abi_version'); + } on ArgumentError { + throw StateError( + 'the loaded WeaveFFI library predates ABI versioning ' + '(these bindings expect ABI revision $_abiVersion)', + ); + } + final found = abiVersion(); + if (found != _abiVersion) { + throw StateError( + 'WeaveFFI ABI mismatch: these bindings expect revision $_abiVersion ' + 'but the loaded library reports revision $found', + ); + } + return lib; +} + +final DynamicLibrary _lib = _checkAbiVersion(_openLibrary()); final class _WeaveFFIError extends Struct { @Int32() @@ -496,9 +519,9 @@ class Notebook { final data = _copyNativeBytes(result, n); if (result != nullptr) _weaveffiFreeBytes(result, n); final reader = _BufferReader(data); - final value = _unpackDocument(reader); + final decoded = _unpackDocument(reader); reader.expectEnd(); - return value; + return decoded; } finally { calloc.free(outLen); calloc.free(err); @@ -587,9 +610,9 @@ Document createDocument(String title, List tags) { final data = _copyNativeBytes(result, n); if (result != nullptr) _weaveffiFreeBytes(result, n); final reader = _BufferReader(data); - final value = _unpackDocument(reader); + final decoded = _unpackDocument(reader); reader.expectEnd(); - return value; + return decoded; } finally { calloc.free(titlePtr); calloc.free(tagsPtr); @@ -614,9 +637,9 @@ Document? getDocument(int id) { final data = _copyNativeBytes(result, n); if (result != nullptr) _weaveffiFreeBytes(result, n); final reader = _BufferReader(data); - final value = (reader.readOptionFlag() ? _unpackDocument(reader) : null); + final decoded = (reader.readOptionFlag() ? _unpackDocument(reader) : null); reader.expectEnd(); - return value; + return decoded; } finally { calloc.free(outLen); calloc.free(err); diff --git a/crates/weaveffi-cli/tests/snapshots/dart_async_demo__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__README_md.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/dart_async_demo__README_md.snap rename to crates/weaveffi-cli/tests/snapshots/dart_edge_cases__README_md.snap diff --git a/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__lib_weaveffi_dart.snap new file mode 100644 index 00000000..abf658e6 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__lib_weaveffi_dart.snap @@ -0,0 +1,1790 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from weaveffi.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate weaveffi.yml -o + +// ignore_for_file: non_constant_identifier_names, camel_case_types, unused_element + +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io' show Platform; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; + +DynamicLibrary _openLibrary() { + // An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a + // specific build artifact regardless of its file name or location. + final override = Platform.environment['WEAVEFFI_LIBRARY']; + if (override != null && override.isNotEmpty) return DynamicLibrary.open(override); + if (Platform.isMacOS) return DynamicLibrary.open('libweaveffi.dylib'); + if (Platform.isLinux) return DynamicLibrary.open('libweaveffi.so'); + if (Platform.isWindows) return DynamicLibrary.open('weaveffi.dll'); + throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}'); +} + +// The ABI revision these bindings were generated against. +const int _abiVersion = 1; + +DynamicLibrary _checkAbiVersion(DynamicLibrary lib) { + final int Function() abiVersion; + try { + abiVersion = lib.lookupFunction('weaveffi_abi_version'); + } on ArgumentError { + throw StateError( + 'the loaded WeaveFFI library predates ABI versioning ' + '(these bindings expect ABI revision $_abiVersion)', + ); + } + final found = abiVersion(); + if (found != _abiVersion) { + throw StateError( + 'WeaveFFI ABI mismatch: these bindings expect revision $_abiVersion ' + 'but the loaded library reports revision $found', + ); + } + return lib; +} + +final DynamicLibrary _lib = _checkAbiVersion(_openLibrary()); + +final class _WeaveFFIError extends Struct { + @Int32() + external int code; + external Pointer message; + external Pointer payloadPtr; + @Size() + external int payloadLen; +} + +typedef _NativeWeaveffiErrorClear = Void Function(Pointer<_WeaveFFIError>); +typedef _DartWeaveffiErrorClear = void Function(Pointer<_WeaveFFIError>); +final _weaveffiErrorClear = _lib.lookupFunction< + _NativeWeaveffiErrorClear, _DartWeaveffiErrorClear>('weaveffi_error_clear'); + +typedef _NativeWeaveffiErrorFree = Void Function(Pointer<_WeaveFFIError>); +typedef _DartWeaveffiErrorFree = void Function(Pointer<_WeaveFFIError>); +final _weaveffiErrorFree = _lib.lookupFunction< + _NativeWeaveffiErrorFree, _DartWeaveffiErrorFree>('weaveffi_error_free'); + +typedef _NativeWeaveffiFreeString = Void Function(Pointer); +typedef _DartWeaveffiFreeString = void Function(Pointer); +final _weaveffiFreeString = _lib.lookupFunction< + _NativeWeaveffiFreeString, _DartWeaveffiFreeString>('weaveffi_free_string'); + +typedef _NativeWeaveffiFreeBytes = Void Function(Pointer, Size); +typedef _DartWeaveffiFreeBytes = void Function(Pointer, int); +final _weaveffiFreeBytes = _lib.lookupFunction< + _NativeWeaveffiFreeBytes, _DartWeaveffiFreeBytes>('weaveffi_free_bytes'); + +/// Generic WeaveFFI failure: panics, marshalling errors, and unknown codes. +class WeaveFFIException implements Exception { + final int code; + final String message; + WeaveFFIException(this.code, this.message); + @override + String toString() => '$runtimeType($code): $message'; +} + +void _checkError(Pointer<_WeaveFFIError> err) { + if (err.ref.code != 0) { + final code = err.ref.code; + final msg = err.ref.message.toDartString(); + _weaveffiErrorClear(err); + throw WeaveFFIException(code, msg); + } +} + +// ── WeaveFFI value-buffer runtime ── +// 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 producer/consumer contract violation, never a typed domain error. + +Never _bufferError(String context) => + throw StateError('malformed WeaveFFI value buffer: $context'); + +// Copies a borrowed native (ptr, len) buffer into Dart-owned memory. +Uint8List _copyNativeBytes(Pointer ptr, int len) => + ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); + +// Stages an encoding into native memory for a borrowed (ptr, len) argument; +// the caller frees the pointer after the call returns. +Pointer _stageBytes(Uint8List bytes) { + final ptr = calloc(bytes.isEmpty ? 1 : bytes.length); + if (bytes.isNotEmpty) ptr.asTypedList(bytes.length).setAll(0, bytes); + return ptr; +} + +final class _BufferWriter { + Uint8List _buf = Uint8List(64); + ByteData? _view; + int _len = 0; + + ByteData get _data => _view ??= ByteData.sublistView(_buf); + + void _ensure(int extra) { + if (_len + extra <= _buf.length) return; + var cap = _buf.length * 2; + while (cap < _len + extra) { + cap *= 2; + } + final next = Uint8List(cap); + next.setRange(0, _len, _buf); + _buf = next; + _view = null; + } + + Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); + + void writeBool(bool v) => writeUint8(v ? 1 : 0); + + void writeOptionFlag(bool present) => writeUint8(present ? 1 : 0); + + void writeInt8(int v) { + _ensure(1); + _data.setInt8(_len, v); + _len += 1; + } + + void writeUint8(int v) { + _ensure(1); + _data.setUint8(_len, v); + _len += 1; + } + + void writeInt16(int v) { + _ensure(2); + _data.setInt16(_len, v, Endian.little); + _len += 2; + } + + void writeUint16(int v) { + _ensure(2); + _data.setUint16(_len, v, Endian.little); + _len += 2; + } + + void writeInt32(int v) { + _ensure(4); + _data.setInt32(_len, v, Endian.little); + _len += 4; + } + + void writeUint32(int v) { + _ensure(4); + _data.setUint32(_len, v, Endian.little); + _len += 4; + } + + void writeInt64(int v) { + _ensure(8); + _data.setInt64(_len, v, Endian.little); + _len += 8; + } + + void writeUint64(int v) { + _ensure(8); + _data.setUint64(_len, v, Endian.little); + _len += 8; + } + + void writeFloat32(double v) { + _ensure(4); + _data.setFloat32(_len, v, Endian.little); + _len += 4; + } + + void writeFloat64(double v) { + _ensure(8); + _data.setFloat64(_len, v, Endian.little); + _len += 8; + } + + void writeLength(int v) => writeUint32(v); + + void writeString(String v) { + final bytes = utf8.encode(v); + writeLength(bytes.length); + _ensure(bytes.length); + _buf.setRange(_len, _len + bytes.length, bytes); + _len += bytes.length; + } + + void writeBytes(List v) { + writeLength(v.length); + _ensure(v.length); + _buf.setRange(_len, _len + v.length, v); + _len += v.length; + } +} + +final class _BufferReader { + final Uint8List _buf; + final ByteData _data; + int _pos = 0; + + _BufferReader(Uint8List buf) + : _buf = buf, + _data = ByteData.sublistView(buf); + + int get _remaining => _buf.length - _pos; + + void _require(int n, String context) { + if (_remaining < n) _bufferError(context); + } + + bool readBool() { + _require(1, 'bool'); + final b = _buf[_pos++]; + if (b > 1) _bufferError('bool byte out of range'); + return b == 1; + } + + bool readOptionFlag() { + _require(1, 'option flag'); + final b = _buf[_pos++]; + if (b > 1) _bufferError('option flag byte out of range'); + return b == 1; + } + + int readInt8() { + _require(1, 'i8'); + return _data.getInt8(_pos++); + } + + int readUint8() { + _require(1, 'u8'); + return _buf[_pos++]; + } + + int readInt16() { + _require(2, 'i16'); + final v = _data.getInt16(_pos, Endian.little); + _pos += 2; + return v; + } + + int readUint16() { + _require(2, 'u16'); + final v = _data.getUint16(_pos, Endian.little); + _pos += 2; + return v; + } + + int readInt32() { + _require(4, 'i32'); + final v = _data.getInt32(_pos, Endian.little); + _pos += 4; + return v; + } + + int readUint32() { + _require(4, 'u32'); + final v = _data.getUint32(_pos, Endian.little); + _pos += 4; + return v; + } + + int readInt64() { + _require(8, 'i64'); + final v = _data.getInt64(_pos, Endian.little); + _pos += 8; + return v; + } + + int readUint64() { + _require(8, 'u64'); + final v = _data.getUint64(_pos, Endian.little); + _pos += 8; + return v; + } + + double readFloat32() { + _require(4, 'f32'); + final v = _data.getFloat32(_pos, Endian.little); + _pos += 4; + return v; + } + + double readFloat64() { + _require(8, 'f64'); + final v = _data.getFloat64(_pos, Endian.little); + _pos += 8; + return v; + } + + int readLength() { + final n = readUint32(); + if (n > _remaining) _bufferError('length prefix exceeds remaining buffer'); + return n; + } + + String readString() { + final n = readLength(); + final s = utf8.decode(Uint8List.sublistView(_buf, _pos, _pos + n)); + _pos += n; + return s; + } + + Uint8List readBytes() { + final n = readLength(); + final b = Uint8List.fromList(Uint8List.sublistView(_buf, _pos, _pos + n)); + _pos += n; + return b; + } + + void expectEnd() { + if (_remaining != 0) _bufferError('trailing bytes after value'); + } +} + +// Live listener trampolines by subscription id. Holding the +// NativeCallable here keeps its native thunk alive until unregistered. +final Map _listenerCallables = {}; + +// Anchors one live native iteration for its GC-finalizer backstop. +// A suspended `sync*` frame keeps the anchor reachable; abandoning the +// iteration drops the frame, and the finalizer destroys the native +// iterator handle. Exhausted iterations detach before destroying +// eagerly, so the handle is destroyed exactly once either way. +final class _IteratorLifetime implements Finalizable {} + +/// Typed error domain `EdgeErrors` declared by module `edge_cases`. +class EdgeErrorsException extends WeaveFFIException { + EdgeErrorsException(super.code, super.message); +} + +/// Resource is busy +class BusyException extends EdgeErrorsException { + BusyException([String message = 'Resource is busy']) : super(7, message); +} + +WeaveFFIException _mapEdgeErrorsException(int code, String message, Uint8List payload) { + switch (code) { + case 7: + return BusyException(message); + default: + return WeaveFFIException(code, message); + } +} + +void _checkEdgeErrorsException(Pointer<_WeaveFFIError> err) { + if (err.ref.code != 0) { + final code = err.ref.code; + final msg = err.ref.message.toDartString(); + final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen); + _weaveffiErrorClear(err); + throw _mapEdgeErrorsException(code, msg, payload); + } +} + +/// Deprecated enum with non-contiguous values and keyword-like variants +enum Legacy { + none(0), + default_(5), + class_(10), + ; + const Legacy(this.value); + final int value; + + static Legacy fromValue(int value) => + Legacy.values.firstWhere((e) => e.value == value); +} + +enum Mode { + fast(1), + safe(2), + ; + const Mode(this.value); + final int value; + + static Mode fromValue(int value) => + Mode.values.firstWhere((e) => e.value == value); +} + +class Leaf { + final double weight; + + Leaf({required this.weight}); +} + +void _packLeaf(_BufferWriter w, Leaf v) { + w.writeFloat32(v.weight); +} + +Leaf _unpackLeaf(_BufferReader r) { + return Leaf( + weight: r.readFloat32(), + ); +} + +/// Middle layer of a three-deep struct nesting +class Inner { + final Leaf leaf; + final List leaves; + + Inner({required this.leaf, required this.leaves}); +} + +void _packInner(_BufferWriter w, Inner v) { + _packLeaf(w, v.leaf); + final t0 = v.leaves; + w.writeLength(t0.length); + for (final t1 in t0) { + _packLeaf(w, t1); + } +} + +Inner _unpackInner(_BufferReader r) { + return Inner( + leaf: _unpackLeaf(r), + leaves: List.generate(r.readLength(), (_) => _unpackLeaf(r)), + ); +} + +class Outer { + 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; + + 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}); +} + +void _packOuter(_BufferWriter w, Outer v) { + _packInner(w, v.inner); + final t0 = v.maybeInner; + if (t0 == null) { + w.writeOptionFlag(false); + } else { + w.writeOptionFlag(true); + _packInner(w, t0); + } + final t1 = v.byName; + w.writeLength(t1.length); + for (final t2 in t1.entries) { + w.writeString(t2.key); + _packInner(w, t2.value); + } + final t3 = v.modes; + w.writeLength(t3.length); + for (final t4 in t3) { + w.writeInt32(t4.value); + } + final t5 = v.matrix; + w.writeLength(t5.length); + for (final t6 in t5) { + final t7 = t6; + w.writeLength(t7.length); + for (final t8 in t7) { + w.writeInt32(t8); + } + } + final t9 = v.sparse; + w.writeLength(t9.length); + for (final t10 in t9) { + final t11 = t10; + if (t11 == null) { + w.writeOptionFlag(false); + } else { + w.writeOptionFlag(true); + w.writeInt64(t11); + } + } + final t12 = v.blob; + if (t12 == null) { + w.writeOptionFlag(false); + } else { + w.writeOptionFlag(true); + w.writeBytes(t12); + } + final t13 = v.note; + if (t13 == null) { + w.writeOptionFlag(false); + } else { + w.writeOptionFlag(true); + w.writeString(t13); + } + final t14 = v.byId; + w.writeLength(t14.length); + for (final t15 in t14.entries) { + w.writeInt64(t15.key); + w.writeString(t15.value); + } + w.writeBool(v.hidden); + w.writeUint8(v.v2); +} + +Outer _unpackOuter(_BufferReader r) { + return Outer( + inner: _unpackInner(r), + maybeInner: (r.readOptionFlag() ? _unpackInner(r) : null), + byName: { for (var i = r.readLength(); i > 0; i--) r.readString(): _unpackInner(r) }, + modes: List.generate(r.readLength(), (_) => Mode.fromValue(r.readInt32())), + matrix: List>.generate(r.readLength(), (_) => List.generate(r.readLength(), (_) => r.readInt32())), + sparse: List.generate(r.readLength(), (_) => (r.readOptionFlag() ? r.readInt64() : null)), + blob: (r.readOptionFlag() ? r.readBytes() : null), + note: (r.readOptionFlag() ? r.readString() : null), + byId: { for (var i = r.readLength(); i > 0; i--) r.readInt64(): r.readString() }, + hidden: r.readBool(), + v2: r.readUint8(), + ); +} + +/// Every field is a reserved word somewhere +class Keywords { + 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_; + + 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_}); +} + +void _packKeywords(_BufferWriter w, Keywords v) { + w.writeInt32(v.class_); + w.writeInt32(v.def); + w.writeInt32(v.import); + w.writeInt32(v.from); + w.writeInt32(v.func); + w.writeInt32(v.range); + w.writeInt32(v.register); + w.writeInt32(v.volatile); + w.writeInt32(v.new_); + w.writeInt32(v.default_); + w.writeInt32(v.end); + w.writeInt32(v.in_); + w.writeInt32(v.is_); + w.writeInt32(v.int_); + w.writeInt32(v.var_); + w.writeInt32(v.val); + w.writeInt32(v.self); + w.writeInt32(v.this_); + w.writeInt32(v.object); + w.writeInt32(v.string); + w.writeInt32(v.case_); + w.writeInt32(v.select); + w.writeInt32(v.switch_); + w.writeInt32(v.void_); + w.writeInt32(v.union); + w.writeInt32(v.namespace); + w.writeInt32(v.operator); + w.writeInt32(v.template); + w.writeInt32(v.package); + w.writeInt32(v.protocol); + w.writeInt32(v.guard); + w.writeInt32(v.defer); + w.writeInt32(v.inout); + w.writeInt32(v.internal); + w.writeInt32(v.when); + w.writeInt32(v.fun); + w.writeInt32(v.data); + w.writeInt32(v.late); + w.writeInt32(v.part); + w.writeInt32(v.out); + w.writeInt32(v.ref); + w.writeInt32(v.event); + w.writeInt32(v.base); + w.writeInt32(v.lambda); + w.writeInt32(v.yield); + w.writeInt32(v.nil); + w.writeInt32(v.null_); + w.writeInt32(v.begin); + w.writeInt32(v.rescue); + w.writeInt32(v.module); + w.writeInt32(v.chan); + w.writeInt32(v.go); + w.writeInt32(v.let); + w.writeInt32(v.export); + w.writeInt32(v.delete); + w.writeInt32(v.typedef); + w.writeInt32(v.sizeof); + w.writeInt32(v.static); + w.writeInt32(v.super_); +} + +Keywords _unpackKeywords(_BufferReader r) { + return Keywords( + class_: r.readInt32(), + def: r.readInt32(), + import: r.readInt32(), + from: r.readInt32(), + func: r.readInt32(), + range: r.readInt32(), + register: r.readInt32(), + volatile: r.readInt32(), + new_: r.readInt32(), + default_: r.readInt32(), + end: r.readInt32(), + in_: r.readInt32(), + is_: r.readInt32(), + int_: r.readInt32(), + var_: r.readInt32(), + val: r.readInt32(), + self: r.readInt32(), + this_: r.readInt32(), + object: r.readInt32(), + string: r.readInt32(), + case_: r.readInt32(), + select: r.readInt32(), + switch_: r.readInt32(), + void_: r.readInt32(), + union: r.readInt32(), + namespace: r.readInt32(), + operator: r.readInt32(), + template: r.readInt32(), + package: r.readInt32(), + protocol: r.readInt32(), + guard: r.readInt32(), + defer: r.readInt32(), + inout: r.readInt32(), + internal: r.readInt32(), + when: r.readInt32(), + fun: r.readInt32(), + data: r.readInt32(), + late: r.readInt32(), + part: r.readInt32(), + out: r.readInt32(), + ref: r.readInt32(), + event: r.readInt32(), + base: r.readInt32(), + lambda: r.readInt32(), + yield: r.readInt32(), + nil: r.readInt32(), + null_: r.readInt32(), + begin: r.readInt32(), + rescue: r.readInt32(), + module: r.readInt32(), + chan: r.readInt32(), + go: r.readInt32(), + let: r.readInt32(), + export: r.readInt32(), + delete: r.readInt32(), + typedef: r.readInt32(), + sizeof: r.readInt32(), + static: r.readInt32(), + super_: r.readInt32(), + ); +} + +/// Deprecated struct +class Retired { + final int id; + + Retired({required this.id}); +} + +void _packRetired(_BufferWriter w, Retired v) { + w.writeInt64(v.id); +} + +Retired _unpackRetired(_BufferReader r) { + return Retired( + id: r.readInt64(), + ); +} + +typedef _NativeWeaveffiEdgeCasesWidgetDestroy = Void Function(Pointer); +typedef _DartWeaveffiEdgeCasesWidgetDestroy = void Function(Pointer); +final _weaveffiEdgeCasesWidgetDestroy = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesWidgetDestroy, _DartWeaveffiEdgeCasesWidgetDestroy>('weaveffi_edge_cases_Widget_destroy'); + +typedef _NativeWeaveffiEdgeCasesWidgetRefresh = Void Function(Pointer, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesWidgetRefresh = void Function(Pointer, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesWidgetRefresh = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesWidgetRefresh, _DartWeaveffiEdgeCasesWidgetRefresh>('weaveffi_edge_cases_Widget_refresh'); + +typedef _NativeWeaveffiEdgeCasesWidgetRename = Pointer Function(Pointer, Pointer, Pointer, Size, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesWidgetRename = Pointer Function(Pointer, Pointer, Pointer, int, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesWidgetRename = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesWidgetRename, _DartWeaveffiEdgeCasesWidgetRename>('weaveffi_edge_cases_Widget_rename'); + +typedef _NativeAsyncCb_weaveffi_edge_cases_Widget_fetch = Void Function(Pointer, Pointer<_WeaveFFIError>, Pointer, Size); + +typedef _NativeWeaveffiEdgeCasesWidgetFetchAsync = Void Function(Pointer, Int64, Pointer>, Pointer); +typedef _DartWeaveffiEdgeCasesWidgetFetchAsync = void Function(Pointer, int, Pointer>, Pointer); +final _weaveffiEdgeCasesWidgetFetchAsync = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesWidgetFetchAsync, _DartWeaveffiEdgeCasesWidgetFetchAsync>('weaveffi_edge_cases_Widget_fetch_async'); + +typedef _NativeWeaveffiEdgeCasesWidgetOldName = Pointer Function(Pointer, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesWidgetOldName = Pointer Function(Pointer, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesWidgetOldName = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesWidgetOldName, _DartWeaveffiEdgeCasesWidgetOldName>('weaveffi_edge_cases_Widget_old_name'); + +/// Interface with no constructor; instances come from make_widget +class Widget { + final Pointer _handle; + Widget._(this._handle); + + /// Releases the native object reference. + void dispose() { + _weaveffiEdgeCasesWidgetDestroy(_handle); + } + + /// Void method that can fail + /// + /// Throws [EdgeErrorsException] on domain errors. + void refresh() { + final err = calloc<_WeaveFFIError>(); + try { + _weaveffiEdgeCasesWidgetRefresh(_handle, err); + _checkEdgeErrorsException(err); + } finally { + calloc.free(err); + } + } + + String rename(String class_, String? import) { + final class_Ptr = class_.toNativeUtf8(); + final importWriter = _BufferWriter(); + final t0 = import; + if (t0 == null) { + importWriter.writeOptionFlag(false); + } else { + importWriter.writeOptionFlag(true); + importWriter.writeString(t0); + } + final importBuf = importWriter.takeBytes(); + final importPtr = _stageBytes(importBuf); + final err = calloc<_WeaveFFIError>(); + try { + final result = _weaveffiEdgeCasesWidgetRename(_handle, class_Ptr, importPtr, importBuf.length, err); + _checkError(err); + final decoded = result.toDartString(); + _weaveffiFreeString(result); + return decoded; + } finally { + calloc.free(class_Ptr); + calloc.free(importPtr); + calloc.free(err); + } + } + + /// Async method with a struct result + /// + /// Throws [EdgeErrorsException] on domain errors. + Future fetch(int id) { + final completer = Completer(); + late NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_Widget_fetch> callable; + callable = NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_Widget_fetch>.listener((Pointer context, Pointer<_WeaveFFIError> err, Pointer result, int resultLen) { + try { + if (err.address != 0 && err.ref.code != 0) { + final code = err.ref.code; + final msg = err.ref.message.toDartString(); + final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen); + _weaveffiErrorFree(err); + completer.completeError(_mapEdgeErrorsException(code, msg, payload)); + return; + } + final resultData = _copyNativeBytes(result, resultLen); + _weaveffiFreeBytes(result, resultLen); + final resultReader = _BufferReader(resultData); + final decoded = _unpackOuter(resultReader); + resultReader.expectEnd(); + completer.complete(decoded); + } catch (e) { + completer.completeError(e); + } finally { + callable.close(); + } + }); + try { + _weaveffiEdgeCasesWidgetFetchAsync(_handle, id, callable.nativeFunction, nullptr); + } catch (e) { + callable.close(); + rethrow; + } + return completer.future; + } + + @Deprecated('Use rename instead') + String oldName() { + final err = calloc<_WeaveFFIError>(); + try { + final result = _weaveffiEdgeCasesWidgetOldName(_handle, err); + _checkError(err); + final decoded = result.toDartString(); + _weaveffiFreeString(result); + return decoded; + } finally { + calloc.free(err); + } + } +} + +typedef _NativeWeaveffiEdgeCasesVaultDestroy = Void Function(Pointer); +typedef _DartWeaveffiEdgeCasesVaultDestroy = void Function(Pointer); +final _weaveffiEdgeCasesVaultDestroy = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesVaultDestroy, _DartWeaveffiEdgeCasesVaultDestroy>('weaveffi_edge_cases_Vault_destroy'); + +typedef _NativeWeaveffiEdgeCasesVaultOpen = Pointer Function(Pointer, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesVaultOpen = Pointer Function(Pointer, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesVaultOpen = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesVaultOpen, _DartWeaveffiEdgeCasesVaultOpen>('weaveffi_edge_cases_Vault_open'); + +typedef _NativeWeaveffiEdgeCasesVaultPeek = Pointer Function(Pointer, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesVaultPeek = Pointer Function(Pointer, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesVaultPeek = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesVaultPeek, _DartWeaveffiEdgeCasesVaultPeek>('weaveffi_edge_cases_Vault_peek'); + +typedef _NativeWeaveffiEdgeCasesVaultProbe = Bool Function(Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesVaultProbe = bool Function(Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesVaultProbe = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesVaultProbe, _DartWeaveffiEdgeCasesVaultProbe>('weaveffi_edge_cases_Vault_probe'); + +/// Deprecated interface with a throwing constructor +class Vault { + final Pointer _handle; + Vault._(this._handle); + + /// Releases the native object reference. + void dispose() { + _weaveffiEdgeCasesVaultDestroy(_handle); + } + + /// Fails when the vault is busy + /// + /// Throws [EdgeErrorsException] on domain errors. + factory Vault.open(String path) { + final pathPtr = path.toNativeUtf8(); + final err = calloc<_WeaveFFIError>(); + try { + final result = _weaveffiEdgeCasesVaultOpen(pathPtr, err); + _checkEdgeErrorsException(err); + return Vault._(result); + } finally { + calloc.free(pathPtr); + calloc.free(err); + } + } + + Widget? peek() { + final err = calloc<_WeaveFFIError>(); + try { + final result = _weaveffiEdgeCasesVaultPeek(_handle, err); + _checkError(err); + if (result == nullptr) return null; + return Widget._(result); + } finally { + calloc.free(err); + } + } + + /// Throwing static + /// + /// Throws [EdgeErrorsException] on domain errors. + static bool probe() { + final err = calloc<_WeaveFFIError>(); + try { + final result = _weaveffiEdgeCasesVaultProbe(err); + _checkEdgeErrorsException(err); + return result; + } finally { + calloc.free(err); + } + } +} + +typedef _NativeCb_weaveffi_edge_cases_OnBatch_fn = Void Function(Pointer, Size, Pointer, Size, Pointer, Size, Int32, Pointer); + +typedef _NativeCb_weaveffi_edge_cases_OnTick_fn = Void Function(Pointer); + +typedef _NativeWeaveffiEdgeCasesRegisterBatchListener = Uint64 Function(Pointer>, Pointer); +typedef _DartWeaveffiEdgeCasesRegisterBatchListener = int Function(Pointer>, Pointer); +final _weaveffiEdgeCasesRegisterBatchListener = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesRegisterBatchListener, _DartWeaveffiEdgeCasesRegisterBatchListener>('weaveffi_edge_cases_register_batch_listener'); + +typedef _NativeWeaveffiEdgeCasesUnregisterBatchListener = Void Function(Uint64); +typedef _DartWeaveffiEdgeCasesUnregisterBatchListener = void Function(int); +final _weaveffiEdgeCasesUnregisterBatchListener = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesUnregisterBatchListener, _DartWeaveffiEdgeCasesUnregisterBatchListener>('weaveffi_edge_cases_unregister_batch_listener'); + +/// Registers a OnBatch listener. Returns a subscription id for unregisterBatchListener(). +int registerBatchListener(void Function(List items, List raw, Outer? first, Mode mode) callback) { + final callable = NativeCallable<_NativeCb_weaveffi_edge_cases_OnBatch_fn>.isolateLocal((Pointer itemsPtr, int itemsLen, Pointer rawPtr, int rawLen, Pointer firstPtr, int firstLen, int mode, Pointer context) { + final itemsData = _copyNativeBytes(itemsPtr, itemsLen); + final itemsReader = _BufferReader(itemsData); + final itemsValue = List.generate(itemsReader.readLength(), (_) => _unpackOuter(itemsReader)); + itemsReader.expectEnd(); + final firstData = _copyNativeBytes(firstPtr, firstLen); + final firstReader = _BufferReader(firstData); + final firstValue = (firstReader.readOptionFlag() ? _unpackOuter(firstReader) : null); + firstReader.expectEnd(); + callback(itemsValue, rawPtr == nullptr ? [] : rawPtr.asTypedList(rawLen).toList(), firstValue, Mode.fromValue(mode)); + }); + final id = _weaveffiEdgeCasesRegisterBatchListener(callable.nativeFunction, nullptr); + _listenerCallables[id] = callable; + return id; +} + +/// Unregisters a listener previously registered with registerBatchListener(). +void unregisterBatchListener(int id) { + _weaveffiEdgeCasesUnregisterBatchListener(id); + _listenerCallables.remove(id)?.close(); +} + +typedef _NativeWeaveffiEdgeCasesRegisterTickListener = Uint64 Function(Pointer>, Pointer); +typedef _DartWeaveffiEdgeCasesRegisterTickListener = int Function(Pointer>, Pointer); +final _weaveffiEdgeCasesRegisterTickListener = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesRegisterTickListener, _DartWeaveffiEdgeCasesRegisterTickListener>('weaveffi_edge_cases_register_tick_listener'); + +typedef _NativeWeaveffiEdgeCasesUnregisterTickListener = Void Function(Uint64); +typedef _DartWeaveffiEdgeCasesUnregisterTickListener = void Function(int); +final _weaveffiEdgeCasesUnregisterTickListener = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesUnregisterTickListener, _DartWeaveffiEdgeCasesUnregisterTickListener>('weaveffi_edge_cases_unregister_tick_listener'); + +/// Registers a OnTick listener. Returns a subscription id for unregisterTickListener(). +int registerTickListener(void Function() callback) { + final callable = NativeCallable<_NativeCb_weaveffi_edge_cases_OnTick_fn>.isolateLocal((Pointer context) { + callback(); + }); + final id = _weaveffiEdgeCasesRegisterTickListener(callable.nativeFunction, nullptr); + _listenerCallables[id] = callable; + return id; +} + +/// Unregisters a listener previously registered with registerTickListener(). +void unregisterTickListener(int id) { + _weaveffiEdgeCasesUnregisterTickListener(id); + _listenerCallables.remove(id)?.close(); +} + +typedef _NativeWeaveffiEdgeCasesNoop = Void Function(Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesNoop = void Function(Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesNoop = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesNoop, _DartWeaveffiEdgeCasesNoop>('weaveffi_edge_cases_noop'); + +/// No parameters and no result +void noop() { + final err = calloc<_WeaveFFIError>(); + try { + _weaveffiEdgeCasesNoop(err); + _checkError(err); + } finally { + calloc.free(err); + } +} + +typedef _NativeWeaveffiEdgeCasesSelect = Int32 Function(Int32, Pointer, Pointer, Size, Pointer, Size, Bool, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesSelect = int Function(int, Pointer, Pointer, int, Pointer, int, bool, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesSelect = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesSelect, _DartWeaveffiEdgeCasesSelect>('weaveffi_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 fromPtr = from.toNativeUtf8(); + final funcWriter = _BufferWriter(); + final t0 = func; + funcWriter.writeLength(t0.length); + for (final t1 in t0) { + funcWriter.writeString(t1); + } + final funcBuf = funcWriter.takeBytes(); + final funcPtr = _stageBytes(funcBuf); + final rangeWriter = _BufferWriter(); + final t2 = range; + if (t2 == null) { + rangeWriter.writeOptionFlag(false); + } else { + rangeWriter.writeOptionFlag(true); + rangeWriter.writeInt32(t2); + } + final rangeBuf = rangeWriter.takeBytes(); + final rangePtr = _stageBytes(rangeBuf); + final err = calloc<_WeaveFFIError>(); + try { + final result = _weaveffiEdgeCasesSelect(class_, fromPtr, funcPtr, funcBuf.length, rangePtr, rangeBuf.length, type, err); + _checkError(err); + return result; + } finally { + calloc.free(fromPtr); + calloc.free(funcPtr); + calloc.free(rangePtr); + calloc.free(err); + } +} + +typedef _NativeWeaveffiEdgeCasesNestedOptional = Pointer Function(Pointer, Size, Pointer, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesNestedOptional = Pointer Function(Pointer, int, Pointer, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesNestedOptional = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesNestedOptional, _DartWeaveffiEdgeCasesNestedOptional>('weaveffi_edge_cases_nested_optional'); + +/// Optional list of optional structs, the deepest legal nesting +List? nestedOptional(List? items) { + final itemsWriter = _BufferWriter(); + final t0 = items; + if (t0 == null) { + itemsWriter.writeOptionFlag(false); + } else { + itemsWriter.writeOptionFlag(true); + final t1 = t0; + itemsWriter.writeLength(t1.length); + for (final t2 in t1) { + final t3 = t2; + if (t3 == null) { + itemsWriter.writeOptionFlag(false); + } else { + itemsWriter.writeOptionFlag(true); + _packOuter(itemsWriter, t3); + } + } + } + final itemsBuf = itemsWriter.takeBytes(); + final itemsPtr = _stageBytes(itemsBuf); + final outLen = calloc(); + final err = calloc<_WeaveFFIError>(); + try { + final result = _weaveffiEdgeCasesNestedOptional(itemsPtr, itemsBuf.length, outLen, err); + _checkError(err); + final n = outLen.value; + final data = _copyNativeBytes(result, n); + if (result != nullptr) _weaveffiFreeBytes(result, n); + final reader = _BufferReader(data); + final decoded = (reader.readOptionFlag() ? List.generate(reader.readLength(), (_) => (reader.readOptionFlag() ? _unpackOuter(reader) : null)) : null); + reader.expectEnd(); + return decoded; + } finally { + calloc.free(itemsPtr); + calloc.free(outLen); + calloc.free(err); + } +} + +typedef _NativeWeaveffiEdgeCasesOptionalParams = Pointer Function(Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Size, Pointer, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesOptionalParams = Pointer Function(Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, int, Pointer, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesOptionalParams = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesOptionalParams, _DartWeaveffiEdgeCasesOptionalParams>('weaveffi_edge_cases_optional_params'); + +Mode? optionalParams(String? text, int? count, Outer? outer, List? tags, List? blob, Mode? mode) { + final textWriter = _BufferWriter(); + final t0 = text; + if (t0 == null) { + textWriter.writeOptionFlag(false); + } else { + textWriter.writeOptionFlag(true); + textWriter.writeString(t0); + } + final textBuf = textWriter.takeBytes(); + final textPtr = _stageBytes(textBuf); + final countWriter = _BufferWriter(); + final t1 = count; + if (t1 == null) { + countWriter.writeOptionFlag(false); + } else { + countWriter.writeOptionFlag(true); + countWriter.writeInt32(t1); + } + final countBuf = countWriter.takeBytes(); + final countPtr = _stageBytes(countBuf); + final outerWriter = _BufferWriter(); + final t2 = outer; + if (t2 == null) { + outerWriter.writeOptionFlag(false); + } else { + outerWriter.writeOptionFlag(true); + _packOuter(outerWriter, t2); + } + final outerBuf = outerWriter.takeBytes(); + final outerPtr = _stageBytes(outerBuf); + final tagsWriter = _BufferWriter(); + final t3 = tags; + if (t3 == null) { + tagsWriter.writeOptionFlag(false); + } else { + tagsWriter.writeOptionFlag(true); + final t4 = t3; + tagsWriter.writeLength(t4.length); + for (final t5 in t4) { + tagsWriter.writeString(t5); + } + } + final tagsBuf = tagsWriter.takeBytes(); + final tagsPtr = _stageBytes(tagsBuf); + final blobWriter = _BufferWriter(); + final t6 = blob; + if (t6 == null) { + blobWriter.writeOptionFlag(false); + } else { + blobWriter.writeOptionFlag(true); + blobWriter.writeBytes(t6); + } + final blobBuf = blobWriter.takeBytes(); + final blobPtr = _stageBytes(blobBuf); + final modeWriter = _BufferWriter(); + final t7 = mode; + if (t7 == null) { + modeWriter.writeOptionFlag(false); + } else { + modeWriter.writeOptionFlag(true); + modeWriter.writeInt32(t7.value); + } + final modeBuf = modeWriter.takeBytes(); + final modePtr = _stageBytes(modeBuf); + final outLen = calloc(); + final err = calloc<_WeaveFFIError>(); + try { + final result = _weaveffiEdgeCasesOptionalParams(textPtr, textBuf.length, countPtr, countBuf.length, outerPtr, outerBuf.length, tagsPtr, tagsBuf.length, blobPtr, blobBuf.length, modePtr, modeBuf.length, outLen, err); + _checkError(err); + final n = outLen.value; + final data = _copyNativeBytes(result, n); + if (result != nullptr) _weaveffiFreeBytes(result, n); + final reader = _BufferReader(data); + final decoded = (reader.readOptionFlag() ? Mode.fromValue(reader.readInt32()) : null); + reader.expectEnd(); + return decoded; + } finally { + calloc.free(textPtr); + calloc.free(countPtr); + calloc.free(outerPtr); + calloc.free(tagsPtr); + calloc.free(blobPtr); + calloc.free(modePtr); + calloc.free(outLen); + calloc.free(err); + } +} + +typedef _NativeWeaveffiEdgeCasesMutableParams = Void Function(Pointer, Pointer, Size, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesMutableParams = void Function(Pointer, Pointer, int, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesMutableParams = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesMutableParams, _DartWeaveffiEdgeCasesMutableParams>('weaveffi_edge_cases_mutable_params'); + +/// Write-back parameters +void mutableParams(String text, List blob) { + final textPtr = text.toNativeUtf8(); + final blobPtr = blob.isEmpty ? nullptr : calloc(blob.length); + for (var i = 0; i < blob.length; i++) { blobPtr[i] = blob[i]; } + final err = calloc<_WeaveFFIError>(); + try { + _weaveffiEdgeCasesMutableParams(textPtr, blobPtr, blob.length, err); + _checkError(err); + } finally { + calloc.free(textPtr); + if (blobPtr != nullptr) calloc.free(blobPtr); + calloc.free(err); + } +} + +typedef _NativeWeaveffiEdgeCasesMakeWidget = Pointer Function(Pointer, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesMakeWidget = Pointer Function(Pointer, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesMakeWidget = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesMakeWidget, _DartWeaveffiEdgeCasesMakeWidget>('weaveffi_edge_cases_make_widget'); + +/// Factory for an interface with no constructor +Widget makeWidget(String label) { + final labelPtr = label.toNativeUtf8(); + final err = calloc<_WeaveFFIError>(); + try { + final result = _weaveffiEdgeCasesMakeWidget(labelPtr, err); + _checkError(err); + return Widget._(result); + } finally { + calloc.free(labelPtr); + calloc.free(err); + } +} + +typedef _NativeWeaveffiEdgeCasesFindWidget = Pointer Function(Int64, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesFindWidget = Pointer Function(int, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesFindWidget = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesFindWidget, _DartWeaveffiEdgeCasesFindWidget>('weaveffi_edge_cases_find_widget'); + +/// Optional interface return +/// +/// Throws [EdgeErrorsException] on domain errors. +Widget? findWidget(int id) { + final err = calloc<_WeaveFFIError>(); + try { + final result = _weaveffiEdgeCasesFindWidget(id, err); + _checkEdgeErrorsException(err); + if (result == nullptr) return null; + return Widget._(result); + } finally { + calloc.free(err); + } +} + +typedef _NativeWeaveffiEdgeCasesInspectWidget = Pointer Function(Pointer, Pointer, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesInspectWidget = Pointer Function(Pointer, Pointer, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesInspectWidget = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesInspectWidget, _DartWeaveffiEdgeCasesInspectWidget>('weaveffi_edge_cases_inspect_widget'); + +/// Interface parameter, including an optional one +String inspectWidget(Widget widget, Widget? fallback) { + final err = calloc<_WeaveFFIError>(); + try { + final result = _weaveffiEdgeCasesInspectWidget(widget._handle, fallback?._handle ?? nullptr, err); + _checkError(err); + final decoded = result.toDartString(); + _weaveffiFreeString(result); + return decoded; + } finally { + calloc.free(err); + } +} + +typedef _NativeWeaveffiEdgeCasesTypedHandleParam = Pointer Function(Pointer, Int64, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesTypedHandleParam = Pointer Function(Pointer, int, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesTypedHandleParam = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesTypedHandleParam, _DartWeaveffiEdgeCasesTypedHandleParam>('weaveffi_edge_cases_typed_handle_param'); + +Leaf typedHandleParam(Leaf token, int raw) { + final err = calloc<_WeaveFFIError>(); + try { + final result = _weaveffiEdgeCasesTypedHandleParam(token._handle, raw, err); + _checkError(err); + return Leaf._(result); + } finally { + calloc.free(err); + } +} + +typedef _NativeWeaveffiEdgeCasesStringStream = Pointer Function(Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesStringStream = Pointer Function(Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesStringStream = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesStringStream, _DartWeaveffiEdgeCasesStringStream>('weaveffi_edge_cases_string_stream'); + +typedef _NativeWeaveffiEdgeCasesStringStreamIteratorNext = Int32 Function(Pointer, Pointer>, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesStringStreamIteratorNext = int Function(Pointer, Pointer>, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesStringStreamIteratorNext = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesStringStreamIteratorNext, _DartWeaveffiEdgeCasesStringStreamIteratorNext>('weaveffi_edge_cases_StringStreamIterator_next'); + +typedef _NativeWeaveffiEdgeCasesStringStreamIteratorDestroy = Void Function(Pointer); +typedef _DartWeaveffiEdgeCasesStringStreamIteratorDestroy = void Function(Pointer); +final _weaveffiEdgeCasesStringStreamIteratorDestroy = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesStringStreamIteratorDestroy, _DartWeaveffiEdgeCasesStringStreamIteratorDestroy>('weaveffi_edge_cases_StringStreamIterator_destroy'); +final _weaveffiEdgeCasesStringStreamIteratorDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('weaveffi_edge_cases_StringStreamIterator_destroy')); + +/// Iterator of strings +/// +/// Returns a lazy [Iterable]: elements are pulled from the native +/// iterator one at a time (one native `next` call per element), and +/// iterating the result again launches a fresh native iterator. +/// +/// The native iterator handle is destroyed exactly once: eagerly when +/// the iteration completes or fails, or by a GC finalizer if the +/// iteration is abandoned before it is exhausted. +Iterable stringStream() sync* { + final err = calloc<_WeaveFFIError>(); + final outItem = calloc>(); + Pointer iter = nullptr; + final anchor = _IteratorLifetime(); + try { + iter = _weaveffiEdgeCasesStringStream(err); + _checkError(err); + _weaveffiEdgeCasesStringStreamIteratorDestroyFinalizer.attach(anchor, iter, detach: anchor); + while (_weaveffiEdgeCasesStringStreamIteratorNext(iter, outItem, err) != 0) { + _checkError(err); + final itemPtr = outItem.value; + final item = itemPtr.toDartString(); + _weaveffiFreeString(itemPtr); + yield item; + } + _checkError(err); + } finally { + if (iter != nullptr) { + _weaveffiEdgeCasesStringStreamIteratorDestroyFinalizer.detach(anchor); + _weaveffiEdgeCasesStringStreamIteratorDestroy(iter); + iter = nullptr; + } + calloc.free(outItem); + calloc.free(err); + } +} + +typedef _NativeWeaveffiEdgeCasesScalarStream = Pointer Function(Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesScalarStream = Pointer Function(Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesScalarStream = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesScalarStream, _DartWeaveffiEdgeCasesScalarStream>('weaveffi_edge_cases_scalar_stream'); + +typedef _NativeWeaveffiEdgeCasesScalarStreamIteratorNext = Int32 Function(Pointer, Pointer, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesScalarStreamIteratorNext = int Function(Pointer, Pointer, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesScalarStreamIteratorNext = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesScalarStreamIteratorNext, _DartWeaveffiEdgeCasesScalarStreamIteratorNext>('weaveffi_edge_cases_ScalarStreamIterator_next'); + +typedef _NativeWeaveffiEdgeCasesScalarStreamIteratorDestroy = Void Function(Pointer); +typedef _DartWeaveffiEdgeCasesScalarStreamIteratorDestroy = void Function(Pointer); +final _weaveffiEdgeCasesScalarStreamIteratorDestroy = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesScalarStreamIteratorDestroy, _DartWeaveffiEdgeCasesScalarStreamIteratorDestroy>('weaveffi_edge_cases_ScalarStreamIterator_destroy'); +final _weaveffiEdgeCasesScalarStreamIteratorDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('weaveffi_edge_cases_ScalarStreamIterator_destroy')); + +/// Iterator of scalars +/// +/// Returns a lazy [Iterable]: elements are pulled from the native +/// iterator one at a time (one native `next` call per element), and +/// iterating the result again launches a fresh native iterator. +/// +/// The native iterator handle is destroyed exactly once: eagerly when +/// the iteration completes or fails, or by a GC finalizer if the +/// iteration is abandoned before it is exhausted. +Iterable scalarStream() sync* { + final err = calloc<_WeaveFFIError>(); + final outItem = calloc(); + Pointer iter = nullptr; + final anchor = _IteratorLifetime(); + try { + iter = _weaveffiEdgeCasesScalarStream(err); + _checkError(err); + _weaveffiEdgeCasesScalarStreamIteratorDestroyFinalizer.attach(anchor, iter, detach: anchor); + while (_weaveffiEdgeCasesScalarStreamIteratorNext(iter, outItem, err) != 0) { + _checkError(err); + yield outItem.value; + } + _checkError(err); + } finally { + if (iter != nullptr) { + _weaveffiEdgeCasesScalarStreamIteratorDestroyFinalizer.detach(anchor); + _weaveffiEdgeCasesScalarStreamIteratorDestroy(iter); + iter = nullptr; + } + calloc.free(outItem); + calloc.free(err); + } +} + +typedef _NativeWeaveffiEdgeCasesModeStream = Pointer Function(Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesModeStream = Pointer Function(Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesModeStream = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesModeStream, _DartWeaveffiEdgeCasesModeStream>('weaveffi_edge_cases_mode_stream'); + +typedef _NativeWeaveffiEdgeCasesModeStreamIteratorNext = Int32 Function(Pointer, Pointer, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesModeStreamIteratorNext = int Function(Pointer, Pointer, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesModeStreamIteratorNext = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesModeStreamIteratorNext, _DartWeaveffiEdgeCasesModeStreamIteratorNext>('weaveffi_edge_cases_ModeStreamIterator_next'); + +typedef _NativeWeaveffiEdgeCasesModeStreamIteratorDestroy = Void Function(Pointer); +typedef _DartWeaveffiEdgeCasesModeStreamIteratorDestroy = void Function(Pointer); +final _weaveffiEdgeCasesModeStreamIteratorDestroy = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesModeStreamIteratorDestroy, _DartWeaveffiEdgeCasesModeStreamIteratorDestroy>('weaveffi_edge_cases_ModeStreamIterator_destroy'); +final _weaveffiEdgeCasesModeStreamIteratorDestroyFinalizer = NativeFinalizer( + _lib.lookup)>>('weaveffi_edge_cases_ModeStreamIterator_destroy')); + +/// Iterator of enums +/// +/// Returns a lazy [Iterable]: elements are pulled from the native +/// iterator one at a time (one native `next` call per element), and +/// iterating the result again launches a fresh native iterator. +/// +/// The native iterator handle is destroyed exactly once: eagerly when +/// the iteration completes or fails, or by a GC finalizer if the +/// iteration is abandoned before it is exhausted. +Iterable modeStream() sync* { + final err = calloc<_WeaveFFIError>(); + final outItem = calloc(); + Pointer iter = nullptr; + final anchor = _IteratorLifetime(); + try { + iter = _weaveffiEdgeCasesModeStream(err); + _checkError(err); + _weaveffiEdgeCasesModeStreamIteratorDestroyFinalizer.attach(anchor, iter, detach: anchor); + while (_weaveffiEdgeCasesModeStreamIteratorNext(iter, outItem, err) != 0) { + _checkError(err); + yield Mode.fromValue(outItem.value); + } + _checkError(err); + } finally { + if (iter != nullptr) { + _weaveffiEdgeCasesModeStreamIteratorDestroyFinalizer.detach(anchor); + _weaveffiEdgeCasesModeStreamIteratorDestroy(iter); + iter = nullptr; + } + calloc.free(outItem); + calloc.free(err); + } +} + +typedef _NativeAsyncCb_weaveffi_edge_cases_async_void = Void Function(Pointer, Pointer<_WeaveFFIError>); + +typedef _NativeWeaveffiEdgeCasesAsyncVoidAsync = Void Function(Pointer>, Pointer); +typedef _DartWeaveffiEdgeCasesAsyncVoidAsync = void Function(Pointer>, Pointer); +final _weaveffiEdgeCasesAsyncVoidAsync = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesAsyncVoidAsync, _DartWeaveffiEdgeCasesAsyncVoidAsync>('weaveffi_edge_cases_async_void_async'); + +/// Async with no result +Future asyncVoid() { + final completer = Completer(); + late NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_async_void> callable; + callable = NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_async_void>.listener((Pointer context, Pointer<_WeaveFFIError> err) { + try { + if (err.address != 0 && err.ref.code != 0) { + final code = err.ref.code; + final msg = err.ref.message.toDartString(); + _weaveffiErrorFree(err); + completer.completeError(WeaveFFIException(code, msg)); + return; + } + completer.complete(); + } catch (e) { + completer.completeError(e); + } finally { + callable.close(); + } + }); + try { + _weaveffiEdgeCasesAsyncVoidAsync(callable.nativeFunction, nullptr); + } catch (e) { + callable.close(); + rethrow; + } + return completer.future; +} + +typedef _NativeAsyncCb_weaveffi_edge_cases_async_bytes = Void Function(Pointer, Pointer<_WeaveFFIError>, Pointer, Size); + +typedef _NativeWeaveffiEdgeCasesAsyncBytesAsync = Void Function(Uint32, Pointer>, Pointer); +typedef _DartWeaveffiEdgeCasesAsyncBytesAsync = void Function(int, Pointer>, Pointer); +final _weaveffiEdgeCasesAsyncBytesAsync = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesAsyncBytesAsync, _DartWeaveffiEdgeCasesAsyncBytesAsync>('weaveffi_edge_cases_async_bytes_async'); + +Future> asyncBytes(int size) { + final completer = Completer>(); + late NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_async_bytes> callable; + callable = NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_async_bytes>.listener((Pointer context, Pointer<_WeaveFFIError> err, Pointer result, int resultLen) { + try { + if (err.address != 0 && err.ref.code != 0) { + final code = err.ref.code; + final msg = err.ref.message.toDartString(); + _weaveffiErrorFree(err); + completer.completeError(WeaveFFIException(code, msg)); + return; + } + final resultData = _copyNativeBytes(result, resultLen); + _weaveffiFreeBytes(result, resultLen); + completer.complete(resultData); + } catch (e) { + completer.completeError(e); + } finally { + callable.close(); + } + }); + try { + _weaveffiEdgeCasesAsyncBytesAsync(size, callable.nativeFunction, nullptr); + } catch (e) { + callable.close(); + rethrow; + } + return completer.future; +} + +typedef _NativeAsyncCb_weaveffi_edge_cases_async_struct = Void Function(Pointer, Pointer<_WeaveFFIError>, Pointer, Size); + +typedef _NativeWeaveffiEdgeCasesAsyncStructAsync = Void Function(Pointer>, Pointer); +typedef _DartWeaveffiEdgeCasesAsyncStructAsync = void Function(Pointer>, Pointer); +final _weaveffiEdgeCasesAsyncStructAsync = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesAsyncStructAsync, _DartWeaveffiEdgeCasesAsyncStructAsync>('weaveffi_edge_cases_async_struct_async'); + +/// Throws [EdgeErrorsException] on domain errors. +Future asyncStruct() { + final completer = Completer(); + late NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_async_struct> callable; + callable = NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_async_struct>.listener((Pointer context, Pointer<_WeaveFFIError> err, Pointer result, int resultLen) { + try { + if (err.address != 0 && err.ref.code != 0) { + final code = err.ref.code; + final msg = err.ref.message.toDartString(); + final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen); + _weaveffiErrorFree(err); + completer.completeError(_mapEdgeErrorsException(code, msg, payload)); + return; + } + final resultData = _copyNativeBytes(result, resultLen); + _weaveffiFreeBytes(result, resultLen); + final resultReader = _BufferReader(resultData); + final decoded = _unpackOuter(resultReader); + resultReader.expectEnd(); + completer.complete(decoded); + } catch (e) { + completer.completeError(e); + } finally { + callable.close(); + } + }); + try { + _weaveffiEdgeCasesAsyncStructAsync(callable.nativeFunction, nullptr); + } catch (e) { + callable.close(); + rethrow; + } + return completer.future; +} + +typedef _NativeAsyncCb_weaveffi_edge_cases_async_list = Void Function(Pointer, Pointer<_WeaveFFIError>, Pointer, Size); + +typedef _NativeWeaveffiEdgeCasesAsyncListAsync = Void Function(Pointer, Pointer>, Pointer); +typedef _DartWeaveffiEdgeCasesAsyncListAsync = void Function(Pointer, Pointer>, Pointer); +final _weaveffiEdgeCasesAsyncListAsync = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesAsyncListAsync, _DartWeaveffiEdgeCasesAsyncListAsync>('weaveffi_edge_cases_async_list_async'); + +/// Throws [EdgeErrorsException] on domain errors. +Future> asyncList() { + final completer = Completer>(); + late NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_async_list> callable; + callable = NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_async_list>.listener((Pointer context, Pointer<_WeaveFFIError> err, Pointer result, int resultLen) { + try { + if (err.address != 0 && err.ref.code != 0) { + final code = err.ref.code; + final msg = err.ref.message.toDartString(); + final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen); + _weaveffiErrorFree(err); + completer.completeError(_mapEdgeErrorsException(code, msg, payload)); + return; + } + final resultData = _copyNativeBytes(result, resultLen); + _weaveffiFreeBytes(result, resultLen); + final resultReader = _BufferReader(resultData); + final decoded = List.generate(resultReader.readLength(), (_) => _unpackOuter(resultReader)); + resultReader.expectEnd(); + completer.complete(decoded); + } catch (e) { + completer.completeError(e); + } finally { + callable.close(); + } + }); + try { + _weaveffiEdgeCasesAsyncListAsync(nullptr, callable.nativeFunction, nullptr); + } catch (e) { + callable.close(); + rethrow; + } + return completer.future; +} + +typedef _NativeAsyncCb_weaveffi_edge_cases_async_optional = Void Function(Pointer, Pointer<_WeaveFFIError>, Pointer, Size); + +typedef _NativeWeaveffiEdgeCasesAsyncOptionalAsync = Void Function(Pointer>, Pointer); +typedef _DartWeaveffiEdgeCasesAsyncOptionalAsync = void Function(Pointer>, Pointer); +final _weaveffiEdgeCasesAsyncOptionalAsync = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesAsyncOptionalAsync, _DartWeaveffiEdgeCasesAsyncOptionalAsync>('weaveffi_edge_cases_async_optional_async'); + +Future asyncOptional() { + final completer = Completer(); + late NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_async_optional> callable; + callable = NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_async_optional>.listener((Pointer context, Pointer<_WeaveFFIError> err, Pointer result, int resultLen) { + try { + if (err.address != 0 && err.ref.code != 0) { + final code = err.ref.code; + final msg = err.ref.message.toDartString(); + _weaveffiErrorFree(err); + completer.completeError(WeaveFFIException(code, msg)); + return; + } + final resultData = _copyNativeBytes(result, resultLen); + _weaveffiFreeBytes(result, resultLen); + final resultReader = _BufferReader(resultData); + final decoded = (resultReader.readOptionFlag() ? resultReader.readInt64() : null); + resultReader.expectEnd(); + completer.complete(decoded); + } catch (e) { + completer.completeError(e); + } finally { + callable.close(); + } + }); + try { + _weaveffiEdgeCasesAsyncOptionalAsync(callable.nativeFunction, nullptr); + } catch (e) { + callable.close(); + rethrow; + } + return completer.future; +} + +typedef _NativeAsyncCb_weaveffi_edge_cases_async_enum = Void Function(Pointer, Pointer<_WeaveFFIError>, Int32); + +typedef _NativeWeaveffiEdgeCasesAsyncEnumAsync = Void Function(Pointer>, Pointer); +typedef _DartWeaveffiEdgeCasesAsyncEnumAsync = void Function(Pointer>, Pointer); +final _weaveffiEdgeCasesAsyncEnumAsync = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesAsyncEnumAsync, _DartWeaveffiEdgeCasesAsyncEnumAsync>('weaveffi_edge_cases_async_enum_async'); + +Future asyncEnum() { + final completer = Completer(); + late NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_async_enum> callable; + callable = NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_async_enum>.listener((Pointer context, Pointer<_WeaveFFIError> err, int result) { + try { + if (err.address != 0 && err.ref.code != 0) { + final code = err.ref.code; + final msg = err.ref.message.toDartString(); + _weaveffiErrorFree(err); + completer.completeError(WeaveFFIException(code, msg)); + return; + } + completer.complete(Mode.fromValue(result)); + } catch (e) { + completer.completeError(e); + } finally { + callable.close(); + } + }); + try { + _weaveffiEdgeCasesAsyncEnumAsync(callable.nativeFunction, nullptr); + } catch (e) { + callable.close(); + rethrow; + } + return completer.future; +} + +typedef _NativeAsyncCb_weaveffi_edge_cases_async_keywords = Void Function(Pointer, Pointer<_WeaveFFIError>, Pointer); + +typedef _NativeWeaveffiEdgeCasesAsyncKeywordsAsync = Void Function(Pointer, Int32, Pointer>, Pointer); +typedef _DartWeaveffiEdgeCasesAsyncKeywordsAsync = void Function(Pointer, int, Pointer>, Pointer); +final _weaveffiEdgeCasesAsyncKeywordsAsync = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesAsyncKeywordsAsync, _DartWeaveffiEdgeCasesAsyncKeywordsAsync>('weaveffi_edge_cases_async_keywords_async'); + +Future asyncKeywords(String class_, int import) { + final completer = Completer(); + final class_Ptr = class_.toNativeUtf8(); + late NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_async_keywords> callable; + callable = NativeCallable<_NativeAsyncCb_weaveffi_edge_cases_async_keywords>.listener((Pointer context, Pointer<_WeaveFFIError> err, Pointer result) { + try { + if (err.address != 0 && err.ref.code != 0) { + final code = err.ref.code; + final msg = err.ref.message.toDartString(); + _weaveffiErrorFree(err); + completer.completeError(WeaveFFIException(code, msg)); + return; + } + final decoded = result.toDartString(); + _weaveffiFreeString(result); + completer.complete(decoded); + } catch (e) { + completer.completeError(e); + } finally { + callable.close(); + } + }); + try { + _weaveffiEdgeCasesAsyncKeywordsAsync(class_Ptr, import, callable.nativeFunction, nullptr); + } catch (e) { + callable.close(); + calloc.free(class_Ptr); + rethrow; + } + return completer.future.whenComplete(() { + calloc.free(class_Ptr); + }); +} + +typedef _NativeWeaveffiEdgeCasesKeywordsRoundtrip = Pointer Function(Pointer, Size, Pointer, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesKeywordsRoundtrip = Pointer Function(Pointer, int, Pointer, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesKeywordsRoundtrip = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesKeywordsRoundtrip, _DartWeaveffiEdgeCasesKeywordsRoundtrip>('weaveffi_edge_cases_keywords_roundtrip'); + +Keywords keywordsRoundtrip(Keywords value) { + final valueWriter = _BufferWriter(); + _packKeywords(valueWriter, value); + final valueBuf = valueWriter.takeBytes(); + final valuePtr = _stageBytes(valueBuf); + final outLen = calloc(); + final err = calloc<_WeaveFFIError>(); + try { + final result = _weaveffiEdgeCasesKeywordsRoundtrip(valuePtr, valueBuf.length, outLen, err); + _checkError(err); + final n = outLen.value; + final data = _copyNativeBytes(result, n); + if (result != nullptr) _weaveffiFreeBytes(result, n); + final reader = _BufferReader(data); + final decoded = _unpackKeywords(reader); + reader.expectEnd(); + return decoded; + } finally { + calloc.free(valuePtr); + calloc.free(outLen); + calloc.free(err); + } +} + +typedef _NativeWeaveffiEdgeCasesLegacyValue = Pointer Function(Int32, Pointer, Pointer<_WeaveFFIError>); +typedef _DartWeaveffiEdgeCasesLegacyValue = Pointer Function(int, Pointer, Pointer<_WeaveFFIError>); +final _weaveffiEdgeCasesLegacyValue = _lib.lookupFunction< + _NativeWeaveffiEdgeCasesLegacyValue, _DartWeaveffiEdgeCasesLegacyValue>('weaveffi_edge_cases_legacy_value'); + +@Deprecated('Use Mode-based APIs') +Retired legacyValue(Legacy value) { + final outLen = calloc(); + final err = calloc<_WeaveFFIError>(); + try { + final result = _weaveffiEdgeCasesLegacyValue(value.value, outLen, err); + _checkError(err); + final n = outLen.value; + final data = _copyNativeBytes(result, n); + if (result != nullptr) _weaveffiFreeBytes(result, n); + final reader = _BufferReader(data); + final decoded = _unpackRetired(reader); + reader.expectEnd(); + return decoded; + } finally { + calloc.free(outLen); + calloc.free(err); + } +} + +// END weaveffi.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_async_demo__pubspec_yaml.snap b/crates/weaveffi-cli/tests/snapshots/dart_edge_cases__pubspec_yaml.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/dart_async_demo__pubspec_yaml.snap rename to crates/weaveffi-cli/tests/snapshots/dart_edge_cases__pubspec_yaml.snap diff --git a/crates/weaveffi-cli/tests/snapshots/dart_events__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dart_events__README_md.snap deleted file mode 100644 index daa40993..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_events__README_md.snap +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# weaveffi (Dart) - -Auto-generated Dart bindings using `dart:ffi`. - -## Usage - -1. Place the compiled shared library (`libweaveffi.dylib`, `libweaveffi.so`, - or `weaveffi.dll`) where the Dart process can find it. - -2. Add this package as a dependency and import the bindings: - -```dart -import 'package:weaveffi/weaveffi.dart'; -``` - -3. Call the generated functions directly. The bindings use `dart:ffi` to load - the native library at runtime via `DynamicLibrary.open` and resolve symbols - with `lookupFunction`. - -## Requirements - -- Dart SDK >= 3.0.0 -- The `ffi` package (`^2.0.0`) for `Utf8` and `calloc` helpers. - - diff --git a/crates/weaveffi-cli/tests/snapshots/dart_events__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_events__lib_weaveffi_dart.snap deleted file mode 100644 index 77dc825d..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_events__lib_weaveffi_dart.snap +++ /dev/null @@ -1,431 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// ignore_for_file: non_constant_identifier_names, camel_case_types, unused_element - -import 'dart:convert'; -import 'dart:ffi'; -import 'dart:io' show Platform; -import 'dart:typed_data'; - -import 'package:ffi/ffi.dart'; - -DynamicLibrary _openLibrary() { - // An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a - // specific build artifact regardless of its file name or location. - final override = Platform.environment['WEAVEFFI_LIBRARY']; - if (override != null && override.isNotEmpty) return DynamicLibrary.open(override); - if (Platform.isMacOS) return DynamicLibrary.open('libweaveffi.dylib'); - if (Platform.isLinux) return DynamicLibrary.open('libweaveffi.so'); - if (Platform.isWindows) return DynamicLibrary.open('weaveffi.dll'); - throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}'); -} - -final DynamicLibrary _lib = _openLibrary(); - -final class _WeaveFFIError extends Struct { - @Int32() - external int code; - external Pointer message; - external Pointer payloadPtr; - @Size() - external int payloadLen; -} - -typedef _NativeWeaveffiErrorClear = Void Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiErrorClear = void Function(Pointer<_WeaveFFIError>); -final _weaveffiErrorClear = _lib.lookupFunction< - _NativeWeaveffiErrorClear, _DartWeaveffiErrorClear>('weaveffi_error_clear'); - -typedef _NativeWeaveffiErrorFree = Void Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiErrorFree = void Function(Pointer<_WeaveFFIError>); -final _weaveffiErrorFree = _lib.lookupFunction< - _NativeWeaveffiErrorFree, _DartWeaveffiErrorFree>('weaveffi_error_free'); - -typedef _NativeWeaveffiFreeString = Void Function(Pointer); -typedef _DartWeaveffiFreeString = void Function(Pointer); -final _weaveffiFreeString = _lib.lookupFunction< - _NativeWeaveffiFreeString, _DartWeaveffiFreeString>('weaveffi_free_string'); - -typedef _NativeWeaveffiFreeBytes = Void Function(Pointer, Size); -typedef _DartWeaveffiFreeBytes = void Function(Pointer, int); -final _weaveffiFreeBytes = _lib.lookupFunction< - _NativeWeaveffiFreeBytes, _DartWeaveffiFreeBytes>('weaveffi_free_bytes'); - -/// Generic WeaveFFI failure: panics, marshalling errors, and unknown codes. -class WeaveFFIException implements Exception { - final int code; - final String message; - WeaveFFIException(this.code, this.message); - @override - String toString() => '$runtimeType($code): $message'; -} - -void _checkError(Pointer<_WeaveFFIError> err) { - if (err.ref.code != 0) { - final code = err.ref.code; - final msg = err.ref.message.toDartString(); - _weaveffiErrorClear(err); - throw WeaveFFIException(code, msg); - } -} - -// ── WeaveFFI value-buffer runtime ── -// 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 producer/consumer contract violation, never a typed domain error. - -Never _bufferError(String context) => - throw StateError('malformed WeaveFFI value buffer: $context'); - -// Copies a borrowed native (ptr, len) buffer into Dart-owned memory. -Uint8List _copyNativeBytes(Pointer ptr, int len) => - ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); - -// Stages an encoding into native memory for a borrowed (ptr, len) argument; -// the caller frees the pointer after the call returns. -Pointer _stageBytes(Uint8List bytes) { - final ptr = calloc(bytes.isEmpty ? 1 : bytes.length); - if (bytes.isNotEmpty) ptr.asTypedList(bytes.length).setAll(0, bytes); - return ptr; -} - -final class _BufferWriter { - Uint8List _buf = Uint8List(64); - ByteData? _view; - int _len = 0; - - ByteData get _data => _view ??= ByteData.sublistView(_buf); - - void _ensure(int extra) { - if (_len + extra <= _buf.length) return; - var cap = _buf.length * 2; - while (cap < _len + extra) { - cap *= 2; - } - final next = Uint8List(cap); - next.setRange(0, _len, _buf); - _buf = next; - _view = null; - } - - Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); - - void writeBool(bool v) => writeUint8(v ? 1 : 0); - - void writeOptionFlag(bool present) => writeUint8(present ? 1 : 0); - - void writeInt8(int v) { - _ensure(1); - _data.setInt8(_len, v); - _len += 1; - } - - void writeUint8(int v) { - _ensure(1); - _data.setUint8(_len, v); - _len += 1; - } - - void writeInt16(int v) { - _ensure(2); - _data.setInt16(_len, v, Endian.little); - _len += 2; - } - - void writeUint16(int v) { - _ensure(2); - _data.setUint16(_len, v, Endian.little); - _len += 2; - } - - void writeInt32(int v) { - _ensure(4); - _data.setInt32(_len, v, Endian.little); - _len += 4; - } - - void writeUint32(int v) { - _ensure(4); - _data.setUint32(_len, v, Endian.little); - _len += 4; - } - - void writeInt64(int v) { - _ensure(8); - _data.setInt64(_len, v, Endian.little); - _len += 8; - } - - void writeUint64(int v) { - _ensure(8); - _data.setUint64(_len, v, Endian.little); - _len += 8; - } - - void writeFloat32(double v) { - _ensure(4); - _data.setFloat32(_len, v, Endian.little); - _len += 4; - } - - void writeFloat64(double v) { - _ensure(8); - _data.setFloat64(_len, v, Endian.little); - _len += 8; - } - - void writeLength(int v) => writeUint32(v); - - void writeString(String v) { - final bytes = utf8.encode(v); - writeLength(bytes.length); - _ensure(bytes.length); - _buf.setRange(_len, _len + bytes.length, bytes); - _len += bytes.length; - } - - void writeBytes(List v) { - writeLength(v.length); - _ensure(v.length); - _buf.setRange(_len, _len + v.length, v); - _len += v.length; - } -} - -final class _BufferReader { - final Uint8List _buf; - final ByteData _data; - int _pos = 0; - - _BufferReader(Uint8List buf) - : _buf = buf, - _data = ByteData.sublistView(buf); - - int get _remaining => _buf.length - _pos; - - void _require(int n, String context) { - if (_remaining < n) _bufferError(context); - } - - bool readBool() { - _require(1, 'bool'); - final b = _buf[_pos++]; - if (b > 1) _bufferError('bool byte out of range'); - return b == 1; - } - - bool readOptionFlag() { - _require(1, 'option flag'); - final b = _buf[_pos++]; - if (b > 1) _bufferError('option flag byte out of range'); - return b == 1; - } - - int readInt8() { - _require(1, 'i8'); - return _data.getInt8(_pos++); - } - - int readUint8() { - _require(1, 'u8'); - return _buf[_pos++]; - } - - int readInt16() { - _require(2, 'i16'); - final v = _data.getInt16(_pos, Endian.little); - _pos += 2; - return v; - } - - int readUint16() { - _require(2, 'u16'); - final v = _data.getUint16(_pos, Endian.little); - _pos += 2; - return v; - } - - int readInt32() { - _require(4, 'i32'); - final v = _data.getInt32(_pos, Endian.little); - _pos += 4; - return v; - } - - int readUint32() { - _require(4, 'u32'); - final v = _data.getUint32(_pos, Endian.little); - _pos += 4; - return v; - } - - int readInt64() { - _require(8, 'i64'); - final v = _data.getInt64(_pos, Endian.little); - _pos += 8; - return v; - } - - int readUint64() { - _require(8, 'u64'); - final v = _data.getUint64(_pos, Endian.little); - _pos += 8; - return v; - } - - double readFloat32() { - _require(4, 'f32'); - final v = _data.getFloat32(_pos, Endian.little); - _pos += 4; - return v; - } - - double readFloat64() { - _require(8, 'f64'); - final v = _data.getFloat64(_pos, Endian.little); - _pos += 8; - return v; - } - - int readLength() { - final n = readUint32(); - if (n > _remaining) _bufferError('length prefix exceeds remaining buffer'); - return n; - } - - String readString() { - final n = readLength(); - final s = utf8.decode(Uint8List.sublistView(_buf, _pos, _pos + n)); - _pos += n; - return s; - } - - Uint8List readBytes() { - final n = readLength(); - final b = Uint8List.fromList(Uint8List.sublistView(_buf, _pos, _pos + n)); - _pos += n; - return b; - } - - void expectEnd() { - if (_remaining != 0) _bufferError('trailing bytes after value'); - } -} - -// Live listener trampolines by subscription id. Holding the -// NativeCallable here keeps its native thunk alive until unregistered. -final Map _listenerCallables = {}; - -// Anchors one live native iteration for its GC-finalizer backstop. -// A suspended `sync*` frame keeps the anchor reachable; abandoning the -// iteration drops the frame, and the finalizer destroys the native -// iterator handle. Exhausted iterations detach before destroying -// eagerly, so the handle is destroyed exactly once either way. -final class _IteratorLifetime implements Finalizable {} - -typedef _NativeCb_weaveffi_events_OnMessage_fn = Void Function(Pointer, Pointer); - -typedef _NativeWeaveffiEventsRegisterMessageListener = Uint64 Function(Pointer>, Pointer); -typedef _DartWeaveffiEventsRegisterMessageListener = int Function(Pointer>, Pointer); -final _weaveffiEventsRegisterMessageListener = _lib.lookupFunction< - _NativeWeaveffiEventsRegisterMessageListener, _DartWeaveffiEventsRegisterMessageListener>('weaveffi_events_register_message_listener'); - -typedef _NativeWeaveffiEventsUnregisterMessageListener = Void Function(Uint64); -typedef _DartWeaveffiEventsUnregisterMessageListener = void Function(int); -final _weaveffiEventsUnregisterMessageListener = _lib.lookupFunction< - _NativeWeaveffiEventsUnregisterMessageListener, _DartWeaveffiEventsUnregisterMessageListener>('weaveffi_events_unregister_message_listener'); - -/// Registers a OnMessage listener. Returns a subscription id for unregisterMessageListener(). -int registerMessageListener(void Function(String message) callback) { - final callable = NativeCallable<_NativeCb_weaveffi_events_OnMessage_fn>.isolateLocal((Pointer message, Pointer context) { - callback(message == nullptr ? '' : message.toDartString()); - }); - final id = _weaveffiEventsRegisterMessageListener(callable.nativeFunction, nullptr); - _listenerCallables[id] = callable; - return id; -} - -/// Unregisters a listener previously registered with registerMessageListener(). -void unregisterMessageListener(int id) { - _weaveffiEventsUnregisterMessageListener(id); - _listenerCallables.remove(id)?.close(); -} - -typedef _NativeWeaveffiEventsSendMessage = Void Function(Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiEventsSendMessage = void Function(Pointer, Pointer<_WeaveFFIError>); -final _weaveffiEventsSendMessage = _lib.lookupFunction< - _NativeWeaveffiEventsSendMessage, _DartWeaveffiEventsSendMessage>('weaveffi_events_send_message'); - -/// Send a message, triggering the OnMessage callback -void sendMessage(String text) { - final textPtr = text.toNativeUtf8(); - final err = calloc<_WeaveFFIError>(); - try { - _weaveffiEventsSendMessage(textPtr, err); - _checkError(err); - } finally { - calloc.free(textPtr); - calloc.free(err); - } -} - -typedef _NativeWeaveffiEventsGetMessages = Pointer Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiEventsGetMessages = Pointer Function(Pointer<_WeaveFFIError>); -final _weaveffiEventsGetMessages = _lib.lookupFunction< - _NativeWeaveffiEventsGetMessages, _DartWeaveffiEventsGetMessages>('weaveffi_events_get_messages'); - -typedef _NativeWeaveffiEventsGetMessagesIteratorNext = Int32 Function(Pointer, Pointer>, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiEventsGetMessagesIteratorNext = int Function(Pointer, Pointer>, Pointer<_WeaveFFIError>); -final _weaveffiEventsGetMessagesIteratorNext = _lib.lookupFunction< - _NativeWeaveffiEventsGetMessagesIteratorNext, _DartWeaveffiEventsGetMessagesIteratorNext>('weaveffi_events_GetMessagesIterator_next'); - -typedef _NativeWeaveffiEventsGetMessagesIteratorDestroy = Void Function(Pointer); -typedef _DartWeaveffiEventsGetMessagesIteratorDestroy = void Function(Pointer); -final _weaveffiEventsGetMessagesIteratorDestroy = _lib.lookupFunction< - _NativeWeaveffiEventsGetMessagesIteratorDestroy, _DartWeaveffiEventsGetMessagesIteratorDestroy>('weaveffi_events_GetMessagesIterator_destroy'); -final _weaveffiEventsGetMessagesIteratorDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('weaveffi_events_GetMessagesIterator_destroy')); - -/// Return an iterator over all sent messages -/// -/// Returns a lazy [Iterable]: elements are pulled from the native -/// iterator one at a time (one native `next` call per element), and -/// iterating the result again launches a fresh native iterator. -/// -/// The native iterator handle is destroyed exactly once: eagerly when -/// the iteration completes or fails, or by a GC finalizer if the -/// iteration is abandoned before it is exhausted. -Iterable getMessages() sync* { - final err = calloc<_WeaveFFIError>(); - final outItem = calloc>(); - Pointer iter = nullptr; - final anchor = _IteratorLifetime(); - try { - iter = _weaveffiEventsGetMessages(err); - _checkError(err); - _weaveffiEventsGetMessagesIteratorDestroyFinalizer.attach(anchor, iter, detach: anchor); - while (_weaveffiEventsGetMessagesIteratorNext(iter, outItem, err) != 0) { - _checkError(err); - final itemPtr = outItem.value; - final item = itemPtr.toDartString(); - _weaveffiFreeString(itemPtr); - yield item; - } - _checkError(err); - } finally { - if (iter != nullptr) { - _weaveffiEventsGetMessagesIteratorDestroyFinalizer.detach(anchor); - _weaveffiEventsGetMessagesIteratorDestroy(iter); - iter = nullptr; - } - calloc.free(outItem); - calloc.free(err); - } -} - -// END weaveffi.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_events__pubspec_yaml.snap b/crates/weaveffi-cli/tests/snapshots/dart_events__pubspec_yaml.snap deleted file mode 100644 index 6c4a1026..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_events__pubspec_yaml.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -name: weaveffi -version: 0.1.0 -description: weaveffi bindings generated by WeaveFFI -environment: - sdk: '>=3.0.0 <4.0.0' -dependencies: - ffi: ^2.0.0 - -# END pubspec.yaml diff --git a/crates/weaveffi-cli/tests/snapshots/dart_inventory__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dart_inventory__README_md.snap deleted file mode 100644 index daa40993..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_inventory__README_md.snap +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# weaveffi (Dart) - -Auto-generated Dart bindings using `dart:ffi`. - -## Usage - -1. Place the compiled shared library (`libweaveffi.dylib`, `libweaveffi.so`, - or `weaveffi.dll`) where the Dart process can find it. - -2. Add this package as a dependency and import the bindings: - -```dart -import 'package:weaveffi/weaveffi.dart'; -``` - -3. Call the generated functions directly. The bindings use `dart:ffi` to load - the native library at runtime via `DynamicLibrary.open` and resolve symbols - with `lookupFunction`. - -## Requirements - -- Dart SDK >= 3.0.0 -- The `ffi` package (`^2.0.0`) for `Utf8` and `calloc` helpers. - - diff --git a/crates/weaveffi-cli/tests/snapshots/dart_inventory__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_inventory__lib_weaveffi_dart.snap deleted file mode 100644 index 36da8e2a..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_inventory__lib_weaveffi_dart.snap +++ /dev/null @@ -1,726 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// ignore_for_file: non_constant_identifier_names, camel_case_types, unused_element - -import 'dart:convert'; -import 'dart:ffi'; -import 'dart:io' show Platform; -import 'dart:typed_data'; - -import 'package:ffi/ffi.dart'; - -DynamicLibrary _openLibrary() { - // An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a - // specific build artifact regardless of its file name or location. - final override = Platform.environment['WEAVEFFI_LIBRARY']; - if (override != null && override.isNotEmpty) return DynamicLibrary.open(override); - if (Platform.isMacOS) return DynamicLibrary.open('libweaveffi.dylib'); - if (Platform.isLinux) return DynamicLibrary.open('libweaveffi.so'); - if (Platform.isWindows) return DynamicLibrary.open('weaveffi.dll'); - throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}'); -} - -final DynamicLibrary _lib = _openLibrary(); - -final class _WeaveFFIError extends Struct { - @Int32() - external int code; - external Pointer message; - external Pointer payloadPtr; - @Size() - external int payloadLen; -} - -typedef _NativeWeaveffiErrorClear = Void Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiErrorClear = void Function(Pointer<_WeaveFFIError>); -final _weaveffiErrorClear = _lib.lookupFunction< - _NativeWeaveffiErrorClear, _DartWeaveffiErrorClear>('weaveffi_error_clear'); - -typedef _NativeWeaveffiErrorFree = Void Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiErrorFree = void Function(Pointer<_WeaveFFIError>); -final _weaveffiErrorFree = _lib.lookupFunction< - _NativeWeaveffiErrorFree, _DartWeaveffiErrorFree>('weaveffi_error_free'); - -typedef _NativeWeaveffiFreeString = Void Function(Pointer); -typedef _DartWeaveffiFreeString = void Function(Pointer); -final _weaveffiFreeString = _lib.lookupFunction< - _NativeWeaveffiFreeString, _DartWeaveffiFreeString>('weaveffi_free_string'); - -typedef _NativeWeaveffiFreeBytes = Void Function(Pointer, Size); -typedef _DartWeaveffiFreeBytes = void Function(Pointer, int); -final _weaveffiFreeBytes = _lib.lookupFunction< - _NativeWeaveffiFreeBytes, _DartWeaveffiFreeBytes>('weaveffi_free_bytes'); - -/// Generic WeaveFFI failure: panics, marshalling errors, and unknown codes. -class WeaveFFIException implements Exception { - final int code; - final String message; - WeaveFFIException(this.code, this.message); - @override - String toString() => '$runtimeType($code): $message'; -} - -void _checkError(Pointer<_WeaveFFIError> err) { - if (err.ref.code != 0) { - final code = err.ref.code; - final msg = err.ref.message.toDartString(); - _weaveffiErrorClear(err); - throw WeaveFFIException(code, msg); - } -} - -// ── WeaveFFI value-buffer runtime ── -// 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 producer/consumer contract violation, never a typed domain error. - -Never _bufferError(String context) => - throw StateError('malformed WeaveFFI value buffer: $context'); - -// Copies a borrowed native (ptr, len) buffer into Dart-owned memory. -Uint8List _copyNativeBytes(Pointer ptr, int len) => - ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); - -// Stages an encoding into native memory for a borrowed (ptr, len) argument; -// the caller frees the pointer after the call returns. -Pointer _stageBytes(Uint8List bytes) { - final ptr = calloc(bytes.isEmpty ? 1 : bytes.length); - if (bytes.isNotEmpty) ptr.asTypedList(bytes.length).setAll(0, bytes); - return ptr; -} - -final class _BufferWriter { - Uint8List _buf = Uint8List(64); - ByteData? _view; - int _len = 0; - - ByteData get _data => _view ??= ByteData.sublistView(_buf); - - void _ensure(int extra) { - if (_len + extra <= _buf.length) return; - var cap = _buf.length * 2; - while (cap < _len + extra) { - cap *= 2; - } - final next = Uint8List(cap); - next.setRange(0, _len, _buf); - _buf = next; - _view = null; - } - - Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); - - void writeBool(bool v) => writeUint8(v ? 1 : 0); - - void writeOptionFlag(bool present) => writeUint8(present ? 1 : 0); - - void writeInt8(int v) { - _ensure(1); - _data.setInt8(_len, v); - _len += 1; - } - - void writeUint8(int v) { - _ensure(1); - _data.setUint8(_len, v); - _len += 1; - } - - void writeInt16(int v) { - _ensure(2); - _data.setInt16(_len, v, Endian.little); - _len += 2; - } - - void writeUint16(int v) { - _ensure(2); - _data.setUint16(_len, v, Endian.little); - _len += 2; - } - - void writeInt32(int v) { - _ensure(4); - _data.setInt32(_len, v, Endian.little); - _len += 4; - } - - void writeUint32(int v) { - _ensure(4); - _data.setUint32(_len, v, Endian.little); - _len += 4; - } - - void writeInt64(int v) { - _ensure(8); - _data.setInt64(_len, v, Endian.little); - _len += 8; - } - - void writeUint64(int v) { - _ensure(8); - _data.setUint64(_len, v, Endian.little); - _len += 8; - } - - void writeFloat32(double v) { - _ensure(4); - _data.setFloat32(_len, v, Endian.little); - _len += 4; - } - - void writeFloat64(double v) { - _ensure(8); - _data.setFloat64(_len, v, Endian.little); - _len += 8; - } - - void writeLength(int v) => writeUint32(v); - - void writeString(String v) { - final bytes = utf8.encode(v); - writeLength(bytes.length); - _ensure(bytes.length); - _buf.setRange(_len, _len + bytes.length, bytes); - _len += bytes.length; - } - - void writeBytes(List v) { - writeLength(v.length); - _ensure(v.length); - _buf.setRange(_len, _len + v.length, v); - _len += v.length; - } -} - -final class _BufferReader { - final Uint8List _buf; - final ByteData _data; - int _pos = 0; - - _BufferReader(Uint8List buf) - : _buf = buf, - _data = ByteData.sublistView(buf); - - int get _remaining => _buf.length - _pos; - - void _require(int n, String context) { - if (_remaining < n) _bufferError(context); - } - - bool readBool() { - _require(1, 'bool'); - final b = _buf[_pos++]; - if (b > 1) _bufferError('bool byte out of range'); - return b == 1; - } - - bool readOptionFlag() { - _require(1, 'option flag'); - final b = _buf[_pos++]; - if (b > 1) _bufferError('option flag byte out of range'); - return b == 1; - } - - int readInt8() { - _require(1, 'i8'); - return _data.getInt8(_pos++); - } - - int readUint8() { - _require(1, 'u8'); - return _buf[_pos++]; - } - - int readInt16() { - _require(2, 'i16'); - final v = _data.getInt16(_pos, Endian.little); - _pos += 2; - return v; - } - - int readUint16() { - _require(2, 'u16'); - final v = _data.getUint16(_pos, Endian.little); - _pos += 2; - return v; - } - - int readInt32() { - _require(4, 'i32'); - final v = _data.getInt32(_pos, Endian.little); - _pos += 4; - return v; - } - - int readUint32() { - _require(4, 'u32'); - final v = _data.getUint32(_pos, Endian.little); - _pos += 4; - return v; - } - - int readInt64() { - _require(8, 'i64'); - final v = _data.getInt64(_pos, Endian.little); - _pos += 8; - return v; - } - - int readUint64() { - _require(8, 'u64'); - final v = _data.getUint64(_pos, Endian.little); - _pos += 8; - return v; - } - - double readFloat32() { - _require(4, 'f32'); - final v = _data.getFloat32(_pos, Endian.little); - _pos += 4; - return v; - } - - double readFloat64() { - _require(8, 'f64'); - final v = _data.getFloat64(_pos, Endian.little); - _pos += 8; - return v; - } - - int readLength() { - final n = readUint32(); - if (n > _remaining) _bufferError('length prefix exceeds remaining buffer'); - return n; - } - - String readString() { - final n = readLength(); - final s = utf8.decode(Uint8List.sublistView(_buf, _pos, _pos + n)); - _pos += n; - return s; - } - - Uint8List readBytes() { - final n = readLength(); - final b = Uint8List.fromList(Uint8List.sublistView(_buf, _pos, _pos + n)); - _pos += n; - return b; - } - - void expectEnd() { - if (_remaining != 0) _bufferError('trailing bytes after value'); - } -} - -/// Typed error domain `ProductsError` declared by module `products`. -class ProductsException extends WeaveFFIException { - ProductsException(super.code, super.message); -} - -/// price must be positive -class InvalidPriceException extends ProductsException { - InvalidPriceException([String message = 'price must be positive']) : super(1, message); -} - -/// product not found -class ProductNotFoundException extends ProductsException { - ProductNotFoundException([String message = 'product not found']) : super(2, message); -} - -WeaveFFIException _mapProductsException(int code, String message, Uint8List payload) { - switch (code) { - case 1: - return InvalidPriceException(message); - case 2: - return ProductNotFoundException(message); - default: - return WeaveFFIException(code, message); - } -} - -void _checkProductsException(Pointer<_WeaveFFIError> err) { - if (err.ref.code != 0) { - final code = err.ref.code; - final msg = err.ref.message.toDartString(); - final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen); - _weaveffiErrorClear(err); - throw _mapProductsException(code, msg, payload); - } -} - -enum Category { - electronics(0), - clothing(1), - food(2), - books(3), - ; - const Category(this.value); - final int value; - - static Category fromValue(int value) => - Category.values.firstWhere((e) => e.value == value); -} - -class Product { - final int id; - final String name; - final String? description; - final double price; - final Category category; - final List tags; - - Product({required this.id, required this.name, this.description, required this.price, required this.category, required this.tags}); -} - -void _packProduct(_BufferWriter w, Product v) { - w.writeInt64(v.id); - w.writeString(v.name); - final t0 = v.description; - if (t0 == null) { - w.writeOptionFlag(false); - } else { - w.writeOptionFlag(true); - w.writeString(t0); - } - w.writeFloat64(v.price); - w.writeInt32(v.category.value); - final t1 = v.tags; - w.writeLength(t1.length); - for (final t2 in t1) { - w.writeString(t2); - } -} - -Product _unpackProduct(_BufferReader r) { - return Product( - id: r.readInt64(), - name: r.readString(), - description: (r.readOptionFlag() ? r.readString() : null), - price: r.readFloat64(), - category: Category.fromValue(r.readInt32()), - tags: List.generate(r.readLength(), (_) => r.readString()), - ); -} - -typedef _NativeWeaveffiProductsCatalogDestroy = Void Function(Pointer); -typedef _DartWeaveffiProductsCatalogDestroy = void Function(Pointer); -final _weaveffiProductsCatalogDestroy = _lib.lookupFunction< - _NativeWeaveffiProductsCatalogDestroy, _DartWeaveffiProductsCatalogDestroy>('weaveffi_products_Catalog_destroy'); - -typedef _NativeWeaveffiProductsCatalogNew = Pointer Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiProductsCatalogNew = Pointer Function(Pointer<_WeaveFFIError>); -final _weaveffiProductsCatalogNew = _lib.lookupFunction< - _NativeWeaveffiProductsCatalogNew, _DartWeaveffiProductsCatalogNew>('weaveffi_products_Catalog_new'); - -typedef _NativeWeaveffiProductsCatalogAddProduct = Pointer Function(Pointer, Pointer, Double, Int32, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiProductsCatalogAddProduct = Pointer Function(Pointer, Pointer, double, int, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiProductsCatalogAddProduct = _lib.lookupFunction< - _NativeWeaveffiProductsCatalogAddProduct, _DartWeaveffiProductsCatalogAddProduct>('weaveffi_products_Catalog_add_product'); - -typedef _NativeWeaveffiProductsCatalogGetProduct = Pointer Function(Pointer, Int64, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiProductsCatalogGetProduct = Pointer Function(Pointer, int, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiProductsCatalogGetProduct = _lib.lookupFunction< - _NativeWeaveffiProductsCatalogGetProduct, _DartWeaveffiProductsCatalogGetProduct>('weaveffi_products_Catalog_get_product'); - -typedef _NativeWeaveffiProductsCatalogSearch = Pointer Function(Pointer, Int32, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiProductsCatalogSearch = Pointer Function(Pointer, int, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiProductsCatalogSearch = _lib.lookupFunction< - _NativeWeaveffiProductsCatalogSearch, _DartWeaveffiProductsCatalogSearch>('weaveffi_products_Catalog_search'); - -typedef _NativeWeaveffiProductsCatalogUpdatePrice = Bool Function(Pointer, Int64, Double, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiProductsCatalogUpdatePrice = bool Function(Pointer, int, double, Pointer<_WeaveFFIError>); -final _weaveffiProductsCatalogUpdatePrice = _lib.lookupFunction< - _NativeWeaveffiProductsCatalogUpdatePrice, _DartWeaveffiProductsCatalogUpdatePrice>('weaveffi_products_Catalog_update_price'); - -typedef _NativeWeaveffiProductsCatalogRemove = Bool Function(Pointer, Int64, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiProductsCatalogRemove = bool Function(Pointer, int, Pointer<_WeaveFFIError>); -final _weaveffiProductsCatalogRemove = _lib.lookupFunction< - _NativeWeaveffiProductsCatalogRemove, _DartWeaveffiProductsCatalogRemove>('weaveffi_products_Catalog_remove'); - -/// A product catalog owning its products -class Catalog { - final Pointer _handle; - Catalog._(this._handle); - - /// Releases the native object reference. - void dispose() { - _weaveffiProductsCatalogDestroy(_handle); - } - - factory Catalog() { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiProductsCatalogNew(err); - _checkError(err); - return Catalog._(result); - } finally { - calloc.free(err); - } - } - - /// Throws [ProductsException] on domain errors. - Product addProduct(String name, double price, Category category) { - final namePtr = name.toNativeUtf8(); - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiProductsCatalogAddProduct(_handle, namePtr, price, category.value, outLen, err); - _checkProductsException(err); - final n = outLen.value; - final data = _copyNativeBytes(result, n); - if (result != nullptr) _weaveffiFreeBytes(result, n); - final reader = _BufferReader(data); - final value = _unpackProduct(reader); - reader.expectEnd(); - return value; - } finally { - calloc.free(namePtr); - calloc.free(outLen); - calloc.free(err); - } - } - - /// Throws [ProductsException] on domain errors. - Product getProduct(int id) { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiProductsCatalogGetProduct(_handle, id, outLen, err); - _checkProductsException(err); - final n = outLen.value; - final data = _copyNativeBytes(result, n); - if (result != nullptr) _weaveffiFreeBytes(result, n); - final reader = _BufferReader(data); - final value = _unpackProduct(reader); - reader.expectEnd(); - return value; - } finally { - calloc.free(outLen); - calloc.free(err); - } - } - - List search(Category category) { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiProductsCatalogSearch(_handle, category.value, outLen, err); - _checkError(err); - final n = outLen.value; - final data = _copyNativeBytes(result, n); - if (result != nullptr) _weaveffiFreeBytes(result, n); - final reader = _BufferReader(data); - final value = List.generate(reader.readLength(), (_) => _unpackProduct(reader)); - reader.expectEnd(); - return value; - } finally { - calloc.free(outLen); - calloc.free(err); - } - } - - /// Throws [ProductsException] on domain errors. - bool updatePrice(int id, double price) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiProductsCatalogUpdatePrice(_handle, id, price, err); - _checkProductsException(err); - return result; - } finally { - calloc.free(err); - } - } - - bool remove(int id) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiProductsCatalogRemove(_handle, id, err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } - } -} - -/// Typed error domain `OrdersError` declared by module `orders`. -class OrdersException extends WeaveFFIException { - OrdersException(super.code, super.message); -} - -/// order not found -class OrderNotFoundException extends OrdersException { - OrderNotFoundException([String message = 'order not found']) : super(1, message); -} - -/// order must contain at least one item -class EmptyOrderException extends OrdersException { - EmptyOrderException([String message = 'order must contain at least one item']) : super(2, message); -} - -WeaveFFIException _mapOrdersException(int code, String message, Uint8List payload) { - switch (code) { - case 1: - return OrderNotFoundException(message); - case 2: - return EmptyOrderException(message); - default: - return WeaveFFIException(code, message); - } -} - -void _checkOrdersException(Pointer<_WeaveFFIError> err) { - if (err.ref.code != 0) { - final code = err.ref.code; - final msg = err.ref.message.toDartString(); - final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen); - _weaveffiErrorClear(err); - throw _mapOrdersException(code, msg, payload); - } -} - -class OrderItem { - final int productId; - final int quantity; - final double unitPrice; - - OrderItem({required this.productId, required this.quantity, required this.unitPrice}); -} - -void _packOrderItem(_BufferWriter w, OrderItem v) { - w.writeInt64(v.productId); - w.writeInt32(v.quantity); - w.writeFloat64(v.unitPrice); -} - -OrderItem _unpackOrderItem(_BufferReader r) { - return OrderItem( - productId: r.readInt64(), - quantity: r.readInt32(), - unitPrice: r.readFloat64(), - ); -} - -class Order { - final int id; - final List items; - final double total; - final String status; - - Order({required this.id, required this.items, required this.total, required this.status}); -} - -void _packOrder(_BufferWriter w, Order v) { - w.writeInt64(v.id); - final t0 = v.items; - w.writeLength(t0.length); - for (final t1 in t0) { - _packOrderItem(w, t1); - } - w.writeFloat64(v.total); - w.writeString(v.status); -} - -Order _unpackOrder(_BufferReader r) { - return Order( - id: r.readInt64(), - items: List.generate(r.readLength(), (_) => _unpackOrderItem(r)), - total: r.readFloat64(), - status: r.readString(), - ); -} - -typedef _NativeWeaveffiOrdersCreateOrder = Int64 Function(Pointer, Size, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiOrdersCreateOrder = int Function(Pointer, int, Pointer<_WeaveFFIError>); -final _weaveffiOrdersCreateOrder = _lib.lookupFunction< - _NativeWeaveffiOrdersCreateOrder, _DartWeaveffiOrdersCreateOrder>('weaveffi_orders_create_order'); - -/// Throws [OrdersException] on domain errors. -int createOrder(List items) { - final itemsWriter = _BufferWriter(); - final t0 = items; - itemsWriter.writeLength(t0.length); - for (final t1 in t0) { - _packOrderItem(itemsWriter, t1); - } - final itemsBuf = itemsWriter.takeBytes(); - final itemsPtr = _stageBytes(itemsBuf); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiOrdersCreateOrder(itemsPtr, itemsBuf.length, err); - _checkOrdersException(err); - return result; - } finally { - calloc.free(itemsPtr); - calloc.free(err); - } -} - -typedef _NativeWeaveffiOrdersGetOrder = Pointer Function(Int64, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiOrdersGetOrder = Pointer Function(int, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiOrdersGetOrder = _lib.lookupFunction< - _NativeWeaveffiOrdersGetOrder, _DartWeaveffiOrdersGetOrder>('weaveffi_orders_get_order'); - -/// Throws [OrdersException] on domain errors. -Order getOrder(int id) { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiOrdersGetOrder(id, outLen, err); - _checkOrdersException(err); - final n = outLen.value; - final data = _copyNativeBytes(result, n); - if (result != nullptr) _weaveffiFreeBytes(result, n); - final reader = _BufferReader(data); - final value = _unpackOrder(reader); - reader.expectEnd(); - return value; - } finally { - calloc.free(outLen); - calloc.free(err); - } -} - -typedef _NativeWeaveffiOrdersCancelOrder = Bool Function(Int64, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiOrdersCancelOrder = bool Function(int, Pointer<_WeaveFFIError>); -final _weaveffiOrdersCancelOrder = _lib.lookupFunction< - _NativeWeaveffiOrdersCancelOrder, _DartWeaveffiOrdersCancelOrder>('weaveffi_orders_cancel_order'); - -bool cancelOrder(int id) { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiOrdersCancelOrder(id, err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } -} - -typedef _NativeWeaveffiOrdersAddProductToOrder = Bool Function(Int64, Pointer, Size, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiOrdersAddProductToOrder = bool Function(int, Pointer, int, Pointer<_WeaveFFIError>); -final _weaveffiOrdersAddProductToOrder = _lib.lookupFunction< - _NativeWeaveffiOrdersAddProductToOrder, _DartWeaveffiOrdersAddProductToOrder>('weaveffi_orders_add_product_to_order'); - -bool addProductToOrder(int orderId, Product product) { - final productWriter = _BufferWriter(); - _packProduct(productWriter, product); - final productBuf = productWriter.takeBytes(); - final productPtr = _stageBytes(productBuf); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiOrdersAddProductToOrder(orderId, productPtr, productBuf.length, err); - _checkError(err); - return result; - } finally { - calloc.free(productPtr); - calloc.free(err); - } -} - -// END weaveffi.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_inventory__pubspec_yaml.snap b/crates/weaveffi-cli/tests/snapshots/dart_inventory__pubspec_yaml.snap deleted file mode 100644 index 6c4a1026..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_inventory__pubspec_yaml.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -name: weaveffi -version: 0.1.0 -description: weaveffi bindings generated by WeaveFFI -environment: - sdk: '>=3.0.0 <4.0.0' -dependencies: - ffi: ^2.0.0 - -# END pubspec.yaml diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_weaveffi_dart.snap index 3f9fb87d..7b307015 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_weaveffi_dart.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_kitchen_sink__lib_weaveffi_dart.snap @@ -26,7 +26,30 @@ DynamicLibrary _openLibrary() { throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}'); } -final DynamicLibrary _lib = _openLibrary(); +// The ABI revision these bindings were generated against. +const int _abiVersion = 1; + +DynamicLibrary _checkAbiVersion(DynamicLibrary lib) { + final int Function() abiVersion; + try { + abiVersion = lib.lookupFunction('weaveffi_abi_version'); + } on ArgumentError { + throw StateError( + 'the loaded WeaveFFI library predates ABI versioning ' + '(these bindings expect ABI revision $_abiVersion)', + ); + } + final found = abiVersion(); + if (found != _abiVersion) { + throw StateError( + 'WeaveFFI ABI mismatch: these bindings expect revision $_abiVersion ' + 'but the loaded library reports revision $found', + ); + } + return lib; +} + +final DynamicLibrary _lib = _checkAbiVersion(_openLibrary()); final class _WeaveFFIError extends Struct { @Int32() @@ -360,9 +383,9 @@ String ping() { try { final result = _weaveffiSharedPing(err); _checkError(err); - final value = result.toDartString(); + final decoded = result.toDartString(); _weaveffiFreeString(result); - return value; + return decoded; } finally { calloc.free(err); } @@ -544,9 +567,9 @@ class Gadget { try { final result = _weaveffiKitchenGadgetDescribe(_handle, err); _checkError(err); - final value = result.toDartString(); + final decoded = result.toDartString(); _weaveffiFreeString(result); - return value; + return decoded; } finally { calloc.free(err); } @@ -572,9 +595,9 @@ class Gadget { try { final result = _weaveffiKitchenGadgetVersion(err); _checkError(err); - final value = result.toDartString(); + final decoded = result.toDartString(); _weaveffiFreeString(result); - return value; + return decoded; } finally { calloc.free(err); } @@ -702,9 +725,9 @@ String echoString(String s) { try { final result = _weaveffiKitchenEchoString(sPtr, err); _checkError(err); - final value = result.toDartString(); + final decoded = result.toDartString(); _weaveffiFreeString(result); - return value; + return decoded; } finally { calloc.free(sPtr); calloc.free(err); @@ -748,9 +771,9 @@ String echoBorrowedStr(String s) { try { final result = _weaveffiKitchenEchoBorrowedStr(sPtr, err); _checkError(err); - final value = result.toDartString(); + final decoded = result.toDartString(); _weaveffiFreeString(result); - return value; + return decoded; } finally { calloc.free(sPtr); calloc.free(err); @@ -835,9 +858,9 @@ Item? maybeItem(int id) { final data = _copyNativeBytes(result, n); if (result != nullptr) _weaveffiFreeBytes(result, n); final reader = _BufferReader(data); - final value = (reader.readOptionFlag() ? _unpackItem(reader) : null); + final decoded = (reader.readOptionFlag() ? _unpackItem(reader) : null); reader.expectEnd(); - return value; + return decoded; } finally { calloc.free(outLen); calloc.free(err); @@ -860,9 +883,9 @@ List listItems() { final data = _copyNativeBytes(result, n); if (result != nullptr) _weaveffiFreeBytes(result, n); final reader = _BufferReader(data); - final value = List.generate(reader.readLength(), (_) => _unpackItem(reader)); + final decoded = List.generate(reader.readLength(), (_) => _unpackItem(reader)); reader.expectEnd(); - return value; + return decoded; } finally { calloc.free(outLen); calloc.free(err); @@ -885,9 +908,9 @@ Map getAttrs() { final data = _copyNativeBytes(result, n); if (result != nullptr) _weaveffiFreeBytes(result, n); final reader = _BufferReader(data); - final value = { for (var i = reader.readLength(); i > 0; i--) reader.readString(): reader.readInt32() }; + final decoded = { for (var i = reader.readLength(); i > 0; i--) reader.readString(): reader.readInt32() }; reader.expectEnd(); - return value; + return decoded; } finally { calloc.free(outLen); calloc.free(err); @@ -970,9 +993,9 @@ Token crossModuleToken() { final data = _copyNativeBytes(result, n); if (result != nullptr) _weaveffiFreeBytes(result, n); final reader = _BufferReader(data); - final value = _unpackToken(reader); + final decoded = _unpackToken(reader); reader.expectEnd(); - return value; + return decoded; } finally { calloc.free(outLen); calloc.free(err); @@ -1000,9 +1023,9 @@ Future doAsync(String input) { completer.completeError(WeaveFFIException(code, msg)); return; } - final value = result.toDartString(); + final decoded = result.toDartString(); _weaveffiFreeString(result); - completer.complete(value); + completer.complete(decoded); } catch (e) { completer.completeError(e); } finally { @@ -1042,9 +1065,9 @@ Future doCancellable(String input) { completer.completeError(WeaveFFIException(code, msg)); return; } - final value = result.toDartString(); + final decoded = result.toDartString(); _weaveffiFreeString(result); - completer.complete(value); + completer.complete(decoded); } catch (e) { completer.completeError(e); } finally { @@ -1109,9 +1132,9 @@ String hello() { try { final result = _weaveffiKitchenNestedHello(err); _checkError(err); - final value = result.toDartString(); + final decoded = result.toDartString(); _weaveffiFreeString(result); - return value; + return decoded; } finally { calloc.free(err); } diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kvstore__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dart_kvstore__README_md.snap deleted file mode 100644 index daa40993..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_kvstore__README_md.snap +++ /dev/null @@ -1,32 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# weaveffi (Dart) - -Auto-generated Dart bindings using `dart:ffi`. - -## Usage - -1. Place the compiled shared library (`libweaveffi.dylib`, `libweaveffi.so`, - or `weaveffi.dll`) where the Dart process can find it. - -2. Add this package as a dependency and import the bindings: - -```dart -import 'package:weaveffi/weaveffi.dart'; -``` - -3. Call the generated functions directly. The bindings use `dart:ffi` to load - the native library at runtime via `DynamicLibrary.open` and resolve symbols - with `lookupFunction`. - -## Requirements - -- Dart SDK >= 3.0.0 -- The `ffi` package (`^2.0.0`) for `Utf8` and `calloc` helpers. - - diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kvstore__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_kvstore__lib_weaveffi_dart.snap deleted file mode 100644 index 60864abd..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_kvstore__lib_weaveffi_dart.snap +++ /dev/null @@ -1,840 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// ignore_for_file: non_constant_identifier_names, camel_case_types, unused_element - -import 'dart:async'; -import 'dart:convert'; -import 'dart:ffi'; -import 'dart:io' show Platform; -import 'dart:typed_data'; - -import 'package:ffi/ffi.dart'; - -DynamicLibrary _openLibrary() { - // An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a - // specific build artifact regardless of its file name or location. - final override = Platform.environment['WEAVEFFI_LIBRARY']; - if (override != null && override.isNotEmpty) return DynamicLibrary.open(override); - if (Platform.isMacOS) return DynamicLibrary.open('libweaveffi.dylib'); - if (Platform.isLinux) return DynamicLibrary.open('libweaveffi.so'); - if (Platform.isWindows) return DynamicLibrary.open('weaveffi.dll'); - throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}'); -} - -final DynamicLibrary _lib = _openLibrary(); - -final class _WeaveFFIError extends Struct { - @Int32() - external int code; - external Pointer message; - external Pointer payloadPtr; - @Size() - external int payloadLen; -} - -typedef _NativeWeaveffiErrorClear = Void Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiErrorClear = void Function(Pointer<_WeaveFFIError>); -final _weaveffiErrorClear = _lib.lookupFunction< - _NativeWeaveffiErrorClear, _DartWeaveffiErrorClear>('weaveffi_error_clear'); - -typedef _NativeWeaveffiErrorFree = Void Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiErrorFree = void Function(Pointer<_WeaveFFIError>); -final _weaveffiErrorFree = _lib.lookupFunction< - _NativeWeaveffiErrorFree, _DartWeaveffiErrorFree>('weaveffi_error_free'); - -typedef _NativeWeaveffiFreeString = Void Function(Pointer); -typedef _DartWeaveffiFreeString = void Function(Pointer); -final _weaveffiFreeString = _lib.lookupFunction< - _NativeWeaveffiFreeString, _DartWeaveffiFreeString>('weaveffi_free_string'); - -typedef _NativeWeaveffiFreeBytes = Void Function(Pointer, Size); -typedef _DartWeaveffiFreeBytes = void Function(Pointer, int); -final _weaveffiFreeBytes = _lib.lookupFunction< - _NativeWeaveffiFreeBytes, _DartWeaveffiFreeBytes>('weaveffi_free_bytes'); - -/// Generic WeaveFFI failure: panics, marshalling errors, and unknown codes. -class WeaveFFIException implements Exception { - final int code; - final String message; - WeaveFFIException(this.code, this.message); - @override - String toString() => '$runtimeType($code): $message'; -} - -void _checkError(Pointer<_WeaveFFIError> err) { - if (err.ref.code != 0) { - final code = err.ref.code; - final msg = err.ref.message.toDartString(); - _weaveffiErrorClear(err); - throw WeaveFFIException(code, msg); - } -} - -// ── WeaveFFI value-buffer runtime ── -// 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 producer/consumer contract violation, never a typed domain error. - -Never _bufferError(String context) => - throw StateError('malformed WeaveFFI value buffer: $context'); - -// Copies a borrowed native (ptr, len) buffer into Dart-owned memory. -Uint8List _copyNativeBytes(Pointer ptr, int len) => - ptr == nullptr ? Uint8List(0) : Uint8List.fromList(ptr.asTypedList(len)); - -// Stages an encoding into native memory for a borrowed (ptr, len) argument; -// the caller frees the pointer after the call returns. -Pointer _stageBytes(Uint8List bytes) { - final ptr = calloc(bytes.isEmpty ? 1 : bytes.length); - if (bytes.isNotEmpty) ptr.asTypedList(bytes.length).setAll(0, bytes); - return ptr; -} - -final class _BufferWriter { - Uint8List _buf = Uint8List(64); - ByteData? _view; - int _len = 0; - - ByteData get _data => _view ??= ByteData.sublistView(_buf); - - void _ensure(int extra) { - if (_len + extra <= _buf.length) return; - var cap = _buf.length * 2; - while (cap < _len + extra) { - cap *= 2; - } - final next = Uint8List(cap); - next.setRange(0, _len, _buf); - _buf = next; - _view = null; - } - - Uint8List takeBytes() => Uint8List.sublistView(_buf, 0, _len); - - void writeBool(bool v) => writeUint8(v ? 1 : 0); - - void writeOptionFlag(bool present) => writeUint8(present ? 1 : 0); - - void writeInt8(int v) { - _ensure(1); - _data.setInt8(_len, v); - _len += 1; - } - - void writeUint8(int v) { - _ensure(1); - _data.setUint8(_len, v); - _len += 1; - } - - void writeInt16(int v) { - _ensure(2); - _data.setInt16(_len, v, Endian.little); - _len += 2; - } - - void writeUint16(int v) { - _ensure(2); - _data.setUint16(_len, v, Endian.little); - _len += 2; - } - - void writeInt32(int v) { - _ensure(4); - _data.setInt32(_len, v, Endian.little); - _len += 4; - } - - void writeUint32(int v) { - _ensure(4); - _data.setUint32(_len, v, Endian.little); - _len += 4; - } - - void writeInt64(int v) { - _ensure(8); - _data.setInt64(_len, v, Endian.little); - _len += 8; - } - - void writeUint64(int v) { - _ensure(8); - _data.setUint64(_len, v, Endian.little); - _len += 8; - } - - void writeFloat32(double v) { - _ensure(4); - _data.setFloat32(_len, v, Endian.little); - _len += 4; - } - - void writeFloat64(double v) { - _ensure(8); - _data.setFloat64(_len, v, Endian.little); - _len += 8; - } - - void writeLength(int v) => writeUint32(v); - - void writeString(String v) { - final bytes = utf8.encode(v); - writeLength(bytes.length); - _ensure(bytes.length); - _buf.setRange(_len, _len + bytes.length, bytes); - _len += bytes.length; - } - - void writeBytes(List v) { - writeLength(v.length); - _ensure(v.length); - _buf.setRange(_len, _len + v.length, v); - _len += v.length; - } -} - -final class _BufferReader { - final Uint8List _buf; - final ByteData _data; - int _pos = 0; - - _BufferReader(Uint8List buf) - : _buf = buf, - _data = ByteData.sublistView(buf); - - int get _remaining => _buf.length - _pos; - - void _require(int n, String context) { - if (_remaining < n) _bufferError(context); - } - - bool readBool() { - _require(1, 'bool'); - final b = _buf[_pos++]; - if (b > 1) _bufferError('bool byte out of range'); - return b == 1; - } - - bool readOptionFlag() { - _require(1, 'option flag'); - final b = _buf[_pos++]; - if (b > 1) _bufferError('option flag byte out of range'); - return b == 1; - } - - int readInt8() { - _require(1, 'i8'); - return _data.getInt8(_pos++); - } - - int readUint8() { - _require(1, 'u8'); - return _buf[_pos++]; - } - - int readInt16() { - _require(2, 'i16'); - final v = _data.getInt16(_pos, Endian.little); - _pos += 2; - return v; - } - - int readUint16() { - _require(2, 'u16'); - final v = _data.getUint16(_pos, Endian.little); - _pos += 2; - return v; - } - - int readInt32() { - _require(4, 'i32'); - final v = _data.getInt32(_pos, Endian.little); - _pos += 4; - return v; - } - - int readUint32() { - _require(4, 'u32'); - final v = _data.getUint32(_pos, Endian.little); - _pos += 4; - return v; - } - - int readInt64() { - _require(8, 'i64'); - final v = _data.getInt64(_pos, Endian.little); - _pos += 8; - return v; - } - - int readUint64() { - _require(8, 'u64'); - final v = _data.getUint64(_pos, Endian.little); - _pos += 8; - return v; - } - - double readFloat32() { - _require(4, 'f32'); - final v = _data.getFloat32(_pos, Endian.little); - _pos += 4; - return v; - } - - double readFloat64() { - _require(8, 'f64'); - final v = _data.getFloat64(_pos, Endian.little); - _pos += 8; - return v; - } - - int readLength() { - final n = readUint32(); - if (n > _remaining) _bufferError('length prefix exceeds remaining buffer'); - return n; - } - - String readString() { - final n = readLength(); - final s = utf8.decode(Uint8List.sublistView(_buf, _pos, _pos + n)); - _pos += n; - return s; - } - - Uint8List readBytes() { - final n = readLength(); - final b = Uint8List.fromList(Uint8List.sublistView(_buf, _pos, _pos + n)); - _pos += n; - return b; - } - - void expectEnd() { - if (_remaining != 0) _bufferError('trailing bytes after value'); - } -} - -// Live listener trampolines by subscription id. Holding the -// NativeCallable here keeps its native thunk alive until unregistered. -final Map _listenerCallables = {}; - -// Anchors one live native iteration for its GC-finalizer backstop. -// A suspended `sync*` frame keeps the anchor reachable; abandoning the -// iteration drops the frame, and the finalizer destroys the native -// iterator handle. Exhausted iterations detach before destroying -// eagerly, so the handle is destroyed exactly once either way. -final class _IteratorLifetime implements Finalizable {} - -/// Typed error domain `KvError` declared by module `kv`. -class KvException extends WeaveFFIException { - KvException(super.code, super.message); -} - -/// No entry exists for the requested key -class KeyNotFoundException extends KvException { - KeyNotFoundException([String message = 'key not found']) : super(1001, message); -} - -/// The entry exists but its TTL has elapsed -class ExpiredException extends KvException { - ExpiredException([String message = 'entry expired']) : super(1002, message); -} - -/// The store has reached its capacity limit -class StoreFullException extends KvException { - StoreFullException([String message = 'store has reached capacity']) : super(1003, message); -} - -/// Underlying storage reported an I/O failure -class IoException extends KvException { - IoException([String message = 'I/O failure']) : super(1004, message); -} - -WeaveFFIException _mapKvException(int code, String message, Uint8List payload) { - switch (code) { - case 1001: - return KeyNotFoundException(message); - case 1002: - return ExpiredException(message); - case 1003: - return StoreFullException(message); - case 1004: - return IoException(message); - default: - return WeaveFFIException(code, message); - } -} - -void _checkKvException(Pointer<_WeaveFFIError> err) { - if (err.ref.code != 0) { - final code = err.ref.code; - final msg = err.ref.message.toDartString(); - final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen); - _weaveffiErrorClear(err); - throw _mapKvException(code, msg, payload); - } -} - -/// Persistence semantics applied to a stored entry -enum EntryKind { - /// In-memory only; lost on close - volatile(0), - /// Flushed to durable storage - persistent(1), - /// Persistent and encrypted at rest - encrypted(2), - ; - const EntryKind(this.value); - final int value; - - static EntryKind fromValue(int value) => - EntryKind.values.firstWhere((e) => e.value == value); -} - -/// A single key-value entry persisted in the store -class Entry { - /// Stable monotonic identifier assigned on insert - final int id; - /// UTF-8 lookup key - final String key; - /// Opaque binary payload - final List value; - /// Unix-timestamp seconds when the entry was created - final int createdAt; - /// Optional unix-timestamp seconds at which the entry expires - final int? expiresAt; - /// Free-form labels attached to the entry - final List tags; - /// Arbitrary string-valued metadata pairs - final Map metadata; - - Entry({required this.id, required this.key, required this.value, required this.createdAt, this.expiresAt, required this.tags, required this.metadata}); -} - -void _packEntry(_BufferWriter w, Entry v) { - w.writeInt64(v.id); - w.writeString(v.key); - w.writeBytes(v.value); - w.writeInt64(v.createdAt); - final t0 = v.expiresAt; - if (t0 == null) { - w.writeOptionFlag(false); - } else { - w.writeOptionFlag(true); - w.writeInt64(t0); - } - final t1 = v.tags; - w.writeLength(t1.length); - for (final t2 in t1) { - w.writeString(t2); - } - final t3 = v.metadata; - w.writeLength(t3.length); - for (final t4 in t3.entries) { - w.writeString(t4.key); - w.writeString(t4.value); - } -} - -Entry _unpackEntry(_BufferReader r) { - return Entry( - id: r.readInt64(), - key: r.readString(), - value: r.readBytes(), - createdAt: r.readInt64(), - expiresAt: (r.readOptionFlag() ? r.readInt64() : null), - tags: List.generate(r.readLength(), (_) => r.readString()), - metadata: { for (var i = r.readLength(); i > 0; i--) r.readString(): r.readString() }, - ); -} - -typedef _NativeWeaveffiKvStoreDestroy = Void Function(Pointer); -typedef _DartWeaveffiKvStoreDestroy = void Function(Pointer); -final _weaveffiKvStoreDestroy = _lib.lookupFunction< - _NativeWeaveffiKvStoreDestroy, _DartWeaveffiKvStoreDestroy>('weaveffi_kv_Store_destroy'); - -typedef _NativeWeaveffiKvStoreOpen = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKvStoreOpen = Pointer Function(Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKvStoreOpen = _lib.lookupFunction< - _NativeWeaveffiKvStoreOpen, _DartWeaveffiKvStoreOpen>('weaveffi_kv_Store_open'); - -typedef _NativeWeaveffiKvStorePut = Bool Function(Pointer, Pointer, Pointer, Size, Int32, Pointer, Size, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKvStorePut = bool Function(Pointer, Pointer, Pointer, int, int, Pointer, int, Pointer<_WeaveFFIError>); -final _weaveffiKvStorePut = _lib.lookupFunction< - _NativeWeaveffiKvStorePut, _DartWeaveffiKvStorePut>('weaveffi_kv_Store_put'); - -typedef _NativeWeaveffiKvStoreGet = Pointer Function(Pointer, Pointer, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKvStoreGet = Pointer Function(Pointer, Pointer, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKvStoreGet = _lib.lookupFunction< - _NativeWeaveffiKvStoreGet, _DartWeaveffiKvStoreGet>('weaveffi_kv_Store_get'); - -typedef _NativeWeaveffiKvStoreDelete = Bool Function(Pointer, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKvStoreDelete = bool Function(Pointer, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKvStoreDelete = _lib.lookupFunction< - _NativeWeaveffiKvStoreDelete, _DartWeaveffiKvStoreDelete>('weaveffi_kv_Store_delete'); - -typedef _NativeWeaveffiKvStoreListKeys = Pointer Function(Pointer, Pointer, Size, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKvStoreListKeys = Pointer Function(Pointer, Pointer, int, Pointer<_WeaveFFIError>); -final _weaveffiKvStoreListKeys = _lib.lookupFunction< - _NativeWeaveffiKvStoreListKeys, _DartWeaveffiKvStoreListKeys>('weaveffi_kv_Store_list_keys'); - -typedef _NativeWeaveffiKvStoreListKeysIteratorNext = Int32 Function(Pointer, Pointer>, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKvStoreListKeysIteratorNext = int Function(Pointer, Pointer>, Pointer<_WeaveFFIError>); -final _weaveffiKvStoreListKeysIteratorNext = _lib.lookupFunction< - _NativeWeaveffiKvStoreListKeysIteratorNext, _DartWeaveffiKvStoreListKeysIteratorNext>('weaveffi_kv_Store_ListKeysIterator_next'); - -typedef _NativeWeaveffiKvStoreListKeysIteratorDestroy = Void Function(Pointer); -typedef _DartWeaveffiKvStoreListKeysIteratorDestroy = void Function(Pointer); -final _weaveffiKvStoreListKeysIteratorDestroy = _lib.lookupFunction< - _NativeWeaveffiKvStoreListKeysIteratorDestroy, _DartWeaveffiKvStoreListKeysIteratorDestroy>('weaveffi_kv_Store_ListKeysIterator_destroy'); -final _weaveffiKvStoreListKeysIteratorDestroyFinalizer = NativeFinalizer( - _lib.lookup)>>('weaveffi_kv_Store_ListKeysIterator_destroy')); - -typedef _NativeWeaveffiKvStoreCount = Int64 Function(Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKvStoreCount = int Function(Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKvStoreCount = _lib.lookupFunction< - _NativeWeaveffiKvStoreCount, _DartWeaveffiKvStoreCount>('weaveffi_kv_Store_count'); - -typedef _NativeWeaveffiKvStoreClear = Void Function(Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKvStoreClear = void Function(Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKvStoreClear = _lib.lookupFunction< - _NativeWeaveffiKvStoreClear, _DartWeaveffiKvStoreClear>('weaveffi_kv_Store_clear'); - -typedef _NativeAsyncCb_weaveffi_kv_Store_compact = Void Function(Pointer, Pointer<_WeaveFFIError>, Int64); - -typedef _NativeWeaveffiKvStoreCompactAsync = Void Function(Pointer, Pointer, Pointer>, Pointer); -typedef _DartWeaveffiKvStoreCompactAsync = void Function(Pointer, Pointer, Pointer>, Pointer); -final _weaveffiKvStoreCompactAsync = _lib.lookupFunction< - _NativeWeaveffiKvStoreCompactAsync, _DartWeaveffiKvStoreCompactAsync>('weaveffi_kv_Store_compact_async'); - -typedef _NativeWeaveffiKvStoreLegacyPut = Bool Function(Pointer, Pointer, Pointer, Size, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKvStoreLegacyPut = bool Function(Pointer, Pointer, Pointer, int, Pointer<_WeaveFFIError>); -final _weaveffiKvStoreLegacyPut = _lib.lookupFunction< - _NativeWeaveffiKvStoreLegacyPut, _DartWeaveffiKvStoreLegacyPut>('weaveffi_kv_Store_legacy_put'); - -typedef _NativeWeaveffiKvStoreDefaultCapacity = Int64 Function(Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKvStoreDefaultCapacity = int Function(Pointer<_WeaveFFIError>); -final _weaveffiKvStoreDefaultCapacity = _lib.lookupFunction< - _NativeWeaveffiKvStoreDefaultCapacity, _DartWeaveffiKvStoreDefaultCapacity>('weaveffi_kv_Store_default_capacity'); - -/// An in-memory key-value store owning its entries; destroying the object releases all of its state -class Store { - final Pointer _handle; - Store._(this._handle); - - /// Releases the native object reference. - void dispose() { - _weaveffiKvStoreDestroy(_handle); - } - - /// Open (or create) a store backed by the given filesystem path - /// - /// Throws [KvException] on domain errors. - factory Store.open(String path) { - final pathPtr = path.toNativeUtf8(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKvStoreOpen(pathPtr, err); - _checkKvException(err); - return Store._(result); - } finally { - calloc.free(pathPtr); - calloc.free(err); - } - } - - /// Insert or replace a value, returning true on success - /// - /// Throws [KvException] on domain errors. - bool put(String key, List value, EntryKind kind, int? ttlSeconds) { - final keyPtr = key.toNativeUtf8(); - final valuePtr = value.isEmpty ? nullptr : calloc(value.length); - for (var i = 0; i < value.length; i++) { valuePtr[i] = value[i]; } - final ttlSecondsWriter = _BufferWriter(); - final t0 = ttlSeconds; - if (t0 == null) { - ttlSecondsWriter.writeOptionFlag(false); - } else { - ttlSecondsWriter.writeOptionFlag(true); - ttlSecondsWriter.writeInt64(t0); - } - final ttlSecondsBuf = ttlSecondsWriter.takeBytes(); - final ttlSecondsPtr = _stageBytes(ttlSecondsBuf); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKvStorePut(_handle, keyPtr, valuePtr, value.length, kind.value, ttlSecondsPtr, ttlSecondsBuf.length, err); - _checkKvException(err); - return result; - } finally { - calloc.free(keyPtr); - if (valuePtr != nullptr) calloc.free(valuePtr); - calloc.free(ttlSecondsPtr); - calloc.free(err); - } - } - - /// Look up an entry by key; returns null if missing or expired - /// - /// Throws [KvException] on domain errors. - Entry? get(String key) { - final keyPtr = key.toNativeUtf8(); - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKvStoreGet(_handle, keyPtr, outLen, err); - _checkKvException(err); - final n = outLen.value; - final data = _copyNativeBytes(result, n); - if (result != nullptr) _weaveffiFreeBytes(result, n); - final reader = _BufferReader(data); - final value = (reader.readOptionFlag() ? _unpackEntry(reader) : null); - reader.expectEnd(); - return value; - } finally { - calloc.free(keyPtr); - calloc.free(outLen); - calloc.free(err); - } - } - - /// Remove the entry for the given key, returning true if it existed - /// - /// Throws [KvException] on domain errors. - bool delete(String key) { - final keyPtr = key.toNativeUtf8(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKvStoreDelete(_handle, keyPtr, err); - _checkKvException(err); - return result; - } finally { - calloc.free(keyPtr); - calloc.free(err); - } - } - - /// Stream every key, optionally filtered by a prefix - /// - /// Returns a lazy [Iterable]: elements are pulled from the native - /// iterator one at a time (one native `next` call per element), and - /// iterating the result again launches a fresh native iterator. - /// - /// The native iterator handle is destroyed exactly once: eagerly when - /// the iteration completes or fails, or by a GC finalizer if the - /// iteration is abandoned before it is exhausted. - /// - /// Throws [KvException] on domain errors. - Iterable listKeys(String? prefix) sync* { - final prefixWriter = _BufferWriter(); - final t0 = prefix; - if (t0 == null) { - prefixWriter.writeOptionFlag(false); - } else { - prefixWriter.writeOptionFlag(true); - prefixWriter.writeString(t0); - } - final prefixBuf = prefixWriter.takeBytes(); - final prefixPtr = _stageBytes(prefixBuf); - final err = calloc<_WeaveFFIError>(); - final outItem = calloc>(); - Pointer iter = nullptr; - final anchor = _IteratorLifetime(); - try { - iter = _weaveffiKvStoreListKeys(_handle, prefixPtr, prefixBuf.length, err); - _checkKvException(err); - _weaveffiKvStoreListKeysIteratorDestroyFinalizer.attach(anchor, iter, detach: anchor); - while (_weaveffiKvStoreListKeysIteratorNext(iter, outItem, err) != 0) { - _checkKvException(err); - final itemPtr = outItem.value; - final item = itemPtr.toDartString(); - _weaveffiFreeString(itemPtr); - yield item; - } - _checkKvException(err); - } finally { - if (iter != nullptr) { - _weaveffiKvStoreListKeysIteratorDestroyFinalizer.detach(anchor); - _weaveffiKvStoreListKeysIteratorDestroy(iter); - iter = nullptr; - } - calloc.free(outItem); - calloc.free(prefixPtr); - calloc.free(err); - } - } - - /// Return the number of live entries in the store - int count() { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKvStoreCount(_handle, err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } - } - - /// Drop every entry from the store - void clear() { - final err = calloc<_WeaveFFIError>(); - try { - _weaveffiKvStoreClear(_handle, err); - _checkError(err); - } finally { - calloc.free(err); - } - } - - /// Reclaim space asynchronously; returns the number of bytes reclaimed - /// - /// Throws [KvException] on domain errors. - Future compact() { - final completer = Completer(); - late NativeCallable<_NativeAsyncCb_weaveffi_kv_Store_compact> callable; - callable = NativeCallable<_NativeAsyncCb_weaveffi_kv_Store_compact>.listener((Pointer context, Pointer<_WeaveFFIError> err, int result) { - try { - if (err.address != 0 && err.ref.code != 0) { - final code = err.ref.code; - final msg = err.ref.message.toDartString(); - final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen); - _weaveffiErrorFree(err); - completer.completeError(_mapKvException(code, msg, payload)); - return; - } - completer.complete(result); - } catch (e) { - completer.completeError(e); - } finally { - callable.close(); - } - }); - try { - _weaveffiKvStoreCompactAsync(_handle, nullptr, callable.nativeFunction, nullptr); - } catch (e) { - callable.close(); - rethrow; - } - return completer.future; - } - - /// Legacy single-shot put kept for compatibility - /// - /// Throws [KvException] on domain errors. - @Deprecated('use put() with explicit kind') - bool legacyPut(String key, List value) { - final keyPtr = key.toNativeUtf8(); - final valuePtr = value.isEmpty ? nullptr : calloc(value.length); - for (var i = 0; i < value.length; i++) { valuePtr[i] = value[i]; } - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKvStoreLegacyPut(_handle, keyPtr, valuePtr, value.length, err); - _checkKvException(err); - return result; - } finally { - calloc.free(keyPtr); - if (valuePtr != nullptr) calloc.free(valuePtr); - calloc.free(err); - } - } - - /// The largest number of live entries one store will hold - static int defaultCapacity() { - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKvStoreDefaultCapacity(err); - _checkError(err); - return result; - } finally { - calloc.free(err); - } - } -} - -typedef _NativeCb_weaveffi_kv_OnEvict_fn = Void Function(Pointer, Pointer); - -typedef _NativeWeaveffiKvRegisterEvictionListener = Uint64 Function(Pointer>, Pointer); -typedef _DartWeaveffiKvRegisterEvictionListener = int Function(Pointer>, Pointer); -final _weaveffiKvRegisterEvictionListener = _lib.lookupFunction< - _NativeWeaveffiKvRegisterEvictionListener, _DartWeaveffiKvRegisterEvictionListener>('weaveffi_kv_register_eviction_listener'); - -typedef _NativeWeaveffiKvUnregisterEvictionListener = Void Function(Uint64); -typedef _DartWeaveffiKvUnregisterEvictionListener = void Function(int); -final _weaveffiKvUnregisterEvictionListener = _lib.lookupFunction< - _NativeWeaveffiKvUnregisterEvictionListener, _DartWeaveffiKvUnregisterEvictionListener>('weaveffi_kv_unregister_eviction_listener'); - -/// Subscribe to per-key eviction notifications -/// Registers a OnEvict listener. Returns a subscription id for unregisterEvictionListener(). -int registerEvictionListener(void Function(String key) callback) { - final callable = NativeCallable<_NativeCb_weaveffi_kv_OnEvict_fn>.isolateLocal((Pointer key, Pointer context) { - callback(key == nullptr ? '' : key.toDartString()); - }); - final id = _weaveffiKvRegisterEvictionListener(callable.nativeFunction, nullptr); - _listenerCallables[id] = callable; - return id; -} - -/// Unregisters a listener previously registered with registerEvictionListener(). -void unregisterEvictionListener(int id) { - _weaveffiKvUnregisterEvictionListener(id); - _listenerCallables.remove(id)?.close(); -} - -/// Aggregate store statistics -class Stats { - /// Number of live entries - final int totalEntries; - /// Sum of all value byte lengths - final int totalBytes; - /// Number of entries past their TTL but not yet evicted - final int expiredEntries; - - Stats({required this.totalEntries, required this.totalBytes, required this.expiredEntries}); -} - -void _packStats(_BufferWriter w, Stats v) { - w.writeInt64(v.totalEntries); - w.writeInt64(v.totalBytes); - w.writeInt64(v.expiredEntries); -} - -Stats _unpackStats(_BufferReader r) { - return Stats( - totalEntries: r.readInt64(), - totalBytes: r.readInt64(), - expiredEntries: r.readInt64(), - ); -} - -typedef _NativeWeaveffiKvStatsGetStats = Pointer Function(Pointer, Pointer, Pointer<_WeaveFFIError>); -typedef _DartWeaveffiKvStatsGetStats = Pointer Function(Pointer, Pointer, Pointer<_WeaveFFIError>); -final _weaveffiKvStatsGetStats = _lib.lookupFunction< - _NativeWeaveffiKvStatsGetStats, _DartWeaveffiKvStatsGetStats>('weaveffi_kv_stats_get_stats'); - -/// Snapshot the current store statistics -/// -/// Throws [KvException] on domain errors. -Stats getStats(Store store) { - final outLen = calloc(); - final err = calloc<_WeaveFFIError>(); - try { - final result = _weaveffiKvStatsGetStats(store._handle, outLen, err); - _checkKvException(err); - final n = outLen.value; - final data = _copyNativeBytes(result, n); - if (result != nullptr) _weaveffiFreeBytes(result, n); - final reader = _BufferReader(data); - final value = _unpackStats(reader); - reader.expectEnd(); - return value; - } finally { - calloc.free(outLen); - calloc.free(err); - } -} - -// END weaveffi.dart diff --git a/crates/weaveffi-cli/tests/snapshots/dart_kvstore__pubspec_yaml.snap b/crates/weaveffi-cli/tests/snapshots/dart_kvstore__pubspec_yaml.snap deleted file mode 100644 index 6c4a1026..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dart_kvstore__pubspec_yaml.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -name: weaveffi -version: 0.1.0 -description: weaveffi bindings generated by WeaveFFI -environment: - sdk: '>=3.0.0 <4.0.0' -dependencies: - ffi: ^2.0.0 - -# END pubspec.yaml diff --git a/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_weaveffi_dart.snap index 23a4c6bd..b588f12c 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_weaveffi_dart.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_nested_modules__lib_weaveffi_dart.snap @@ -25,7 +25,30 @@ DynamicLibrary _openLibrary() { throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}'); } -final DynamicLibrary _lib = _openLibrary(); +// The ABI revision these bindings were generated against. +const int _abiVersion = 1; + +DynamicLibrary _checkAbiVersion(DynamicLibrary lib) { + final int Function() abiVersion; + try { + abiVersion = lib.lookupFunction('weaveffi_abi_version'); + } on ArgumentError { + throw StateError( + 'the loaded WeaveFFI library predates ABI versioning ' + '(these bindings expect ABI revision $_abiVersion)', + ); + } + final found = abiVersion(); + if (found != _abiVersion) { + throw StateError( + 'WeaveFFI ABI mismatch: these bindings expect revision $_abiVersion ' + 'but the loaded library reports revision $found', + ); + } + return lib; +} + +final DynamicLibrary _lib = _checkAbiVersion(_openLibrary()); final class _WeaveFFIError extends Struct { @Int32() @@ -395,9 +418,9 @@ Canvas newCanvas(int width) { final data = _copyNativeBytes(result, n); if (result != nullptr) _weaveffiFreeBytes(result, n); final reader = _BufferReader(data); - final value = _unpackCanvas(reader); + final decoded = _unpackCanvas(reader); reader.expectEnd(); - return value; + return decoded; } finally { calloc.free(outLen); calloc.free(err); @@ -445,9 +468,9 @@ Shape makeShape(int sides) { final data = _copyNativeBytes(result, n); if (result != nullptr) _weaveffiFreeBytes(result, n); final reader = _BufferReader(data); - final value = _unpackShape(reader); + final decoded = _unpackShape(reader); reader.expectEnd(); - return value; + return decoded; } finally { calloc.free(outLen); calloc.free(err); @@ -470,9 +493,9 @@ Canvas describe() { final data = _copyNativeBytes(result, n); if (result != nullptr) _weaveffiFreeBytes(result, n); final reader = _BufferReader(data); - final value = _unpackCanvas(reader); + final decoded = _unpackCanvas(reader); reader.expectEnd(); - return value; + return decoded; } finally { calloc.free(outLen); calloc.free(err); @@ -518,9 +541,9 @@ Detail compute(Shape shape) { final data = _copyNativeBytes(result, n); if (result != nullptr) _weaveffiFreeBytes(result, n); final reader = _BufferReader(data); - final value = _unpackDetail(reader); + final decoded = _unpackDetail(reader); reader.expectEnd(); - return value; + return decoded; } finally { calloc.free(shapePtr); calloc.free(outLen); diff --git a/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_weaveffi_dart.snap b/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_weaveffi_dart.snap index 816e7ae7..970a0b13 100644 --- a/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_weaveffi_dart.snap +++ b/crates/weaveffi-cli/tests/snapshots/dart_shapes__lib_weaveffi_dart.snap @@ -25,7 +25,30 @@ DynamicLibrary _openLibrary() { throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}'); } -final DynamicLibrary _lib = _openLibrary(); +// The ABI revision these bindings were generated against. +const int _abiVersion = 1; + +DynamicLibrary _checkAbiVersion(DynamicLibrary lib) { + final int Function() abiVersion; + try { + abiVersion = lib.lookupFunction('weaveffi_abi_version'); + } on ArgumentError { + throw StateError( + 'the loaded WeaveFFI library predates ABI versioning ' + '(these bindings expect ABI revision $_abiVersion)', + ); + } + final found = abiVersion(); + if (found != _abiVersion) { + throw StateError( + 'WeaveFFI ABI mismatch: these bindings expect revision $_abiVersion ' + 'but the loaded library reports revision $found', + ); + } + return lib; +} + +final DynamicLibrary _lib = _checkAbiVersion(_openLibrary()); final class _WeaveFFIError extends Struct { @Int32() @@ -471,9 +494,9 @@ String describe(Shape shape) { try { final result = _weaveffiShapesDescribe(shapePtr, shapeBuf.length, err); _checkError(err); - final value = result.toDartString(); + final decoded = result.toDartString(); _weaveffiFreeString(result); - return value; + return decoded; } finally { calloc.free(shapePtr); calloc.free(err); @@ -500,9 +523,9 @@ Shape scale(Shape shape, double factor) { final data = _copyNativeBytes(result, n); if (result != nullptr) _weaveffiFreeBytes(result, n); final reader = _BufferReader(data); - final value = _unpackShape(reader); + final decoded = _unpackShape(reader); reader.expectEnd(); - return value; + return decoded; } finally { calloc.free(shapePtr); calloc.free(outLen); @@ -546,9 +569,9 @@ Numerics makeNumerics(int a, int b, int c, int d, int e, int f, int g, int h, do final data = _copyNativeBytes(result, n); if (result != nullptr) _weaveffiFreeBytes(result, n); final reader = _BufferReader(data); - final value = _unpackNumerics(reader); + final decoded = _unpackNumerics(reader); reader.expectEnd(); - return value; + return decoded; } finally { calloc.free(outLen); calloc.free(err); diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_async_demo__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_async_demo__WeaveFFI_cs.snap deleted file mode 100644 index 50adeb1a..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_async_demo__WeaveFFI_cs.snap +++ /dev/null @@ -1,645 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Threading.Tasks; - -namespace WeaveFFI -{ - public class WeaveFFIException : Exception - { - public int Code { get; } - - public WeaveFFIException(int code, string message) : base(message) - { - Code = code; - } - } - - /// Typed exception for the TaskError error domain (module tasks). - public class TaskException : WeaveFFIException - { - /// task name must not be empty - public const int InvalidName = 1; - - public TaskException(int code, string message) : base(code, message) - { - } - - /// Wraps a raw error slot in the typed exception, falling - /// back to for unknown codes. Codes - /// declaring payload fields decode them into Data. - internal static WeaveFFIException FromCode(int code, string message, byte[]? payload) - { - switch (code) - { - case InvalidName: - return new TaskException(code, string.IsNullOrEmpty(message) ? "task name must not be empty" : message); - default: - return new WeaveFFIException(code, message); - } - } - } - - [StructLayout(LayoutKind.Sequential)] - internal struct WeaveFFIError - { - public int Code; - public IntPtr Message; - public IntPtr PayloadPtr; - public UIntPtr PayloadLen; - - internal static byte[]? CopyPayload(WeaveFFIError err) - { - if (err.PayloadPtr == IntPtr.Zero || (int)err.PayloadLen == 0) - { - return null; - } - var payload = new byte[(int)err.PayloadLen]; - Marshal.Copy(err.PayloadPtr, payload, 0, (int)err.PayloadLen); - return payload; - } - - internal static void Check(WeaveFFIError err) - { - if (err.Code != 0) - { - var code = err.Code; - var msg = Marshal.PtrToStringUTF8(err.Message) ?? ""; - NativeMethods.weaveffi_error_clear(ref err); - throw new WeaveFFIException(code, msg); - } - } - - internal static void CheckTask(WeaveFFIError err) - { - if (err.Code != 0) - { - var code = err.Code; - var msg = Marshal.PtrToStringUTF8(err.Message) ?? ""; - var payload = CopyPayload(err); - NativeMethods.weaveffi_error_clear(ref err); - throw TaskException.FromCode(code, msg, payload); - } - } - } - - internal static class WeaveFFIHelpers - { - internal static IntPtr StringToPtr(string? s) - { - return s == null ? IntPtr.Zero : Marshal.StringToCoTaskMemUTF8(s); - } - - internal static string? PtrToString(IntPtr ptr) - { - return ptr == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(ptr); - } - - internal static void FreePtr(IntPtr ptr) - { - Marshal.FreeCoTaskMem(ptr); - } - } - - /// Serializes values into the WeaveFFI value-buffer wire - /// format (little-endian, packed). - internal sealed class WeaveFFIBufferWriter - { - private byte[] _buf = new byte[64]; - private int _len; - - private void Ensure(int extra) - { - if (_len + extra <= _buf.Length) - { - return; - } - var size = _buf.Length * 2; - while (size < _len + extra) - { - size *= 2; - } - Array.Resize(ref _buf, size); - } - - internal void WriteBool(bool v) - { - Ensure(1); - _buf[_len++] = v ? (byte)1 : (byte)0; - } - - internal void WriteI8(sbyte v) - { - Ensure(1); - _buf[_len++] = (byte)v; - } - - internal void WriteU8(byte v) - { - Ensure(1); - _buf[_len++] = v; - } - - internal void WriteU16(ushort v) - { - Ensure(2); - _buf[_len++] = (byte)v; - _buf[_len++] = (byte)(v >> 8); - } - - internal void WriteI16(short v) - { - WriteU16((ushort)v); - } - - internal void WriteU32(uint v) - { - Ensure(4); - _buf[_len++] = (byte)v; - _buf[_len++] = (byte)(v >> 8); - _buf[_len++] = (byte)(v >> 16); - _buf[_len++] = (byte)(v >> 24); - } - - internal void WriteI32(int v) - { - WriteU32((uint)v); - } - - internal void WriteU64(ulong v) - { - WriteU32((uint)v); - WriteU32((uint)(v >> 32)); - } - - internal void WriteI64(long v) - { - WriteU64((ulong)v); - } - - internal void WriteF32(float v) - { - WriteU32((uint)BitConverter.SingleToInt32Bits(v)); - } - - internal void WriteF64(double v) - { - WriteU64((ulong)BitConverter.DoubleToInt64Bits(v)); - } - - internal void WriteLen(int len) - { - WriteU32((uint)len); - } - - internal void WriteOptionFlag(bool present) - { - WriteBool(present); - } - - internal void WriteString(string v) - { - var bytes = System.Text.Encoding.UTF8.GetBytes(v); - WriteLen(bytes.Length); - Ensure(bytes.Length); - Array.Copy(bytes, 0, _buf, _len, bytes.Length); - _len += bytes.Length; - } - - internal void WriteBytes(byte[] v) - { - WriteLen(v.Length); - Ensure(v.Length); - Array.Copy(v, 0, _buf, _len, v.Length); - _len += v.Length; - } - - internal byte[] ToArray() - { - var outBuf = new byte[_len]; - Array.Copy(_buf, outBuf, _len); - return outBuf; - } - } - - /// Decodes values from the WeaveFFI value-buffer wire format. - /// A malformed buffer indicates a producer/consumer contract violation and - /// throws . - internal sealed class WeaveFFIBufferReader - { - private static readonly System.Text.Encoding Utf8Strict = - new System.Text.UTF8Encoding(false, true); - - private readonly byte[] _data; - private int _pos; - - internal WeaveFFIBufferReader(byte[] data) - { - _data = data; - } - - private void Require(int n) - { - if (_data.Length - _pos < n) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: buffer exhausted"); - } - } - - internal bool ReadBool() - { - Require(1); - var b = _data[_pos++]; - if (b > 1) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: invalid bool byte"); - } - return b == 1; - } - - internal sbyte ReadI8() - { - Require(1); - return (sbyte)_data[_pos++]; - } - - internal byte ReadU8() - { - Require(1); - return _data[_pos++]; - } - - internal ushort ReadU16() - { - Require(2); - var v = (ushort)(_data[_pos] | (_data[_pos + 1] << 8)); - _pos += 2; - return v; - } - - internal short ReadI16() - { - return (short)ReadU16(); - } - - internal uint ReadU32() - { - Require(4); - var v = (uint)_data[_pos] - | ((uint)_data[_pos + 1] << 8) - | ((uint)_data[_pos + 2] << 16) - | ((uint)_data[_pos + 3] << 24); - _pos += 4; - return v; - } - - internal int ReadI32() - { - return (int)ReadU32(); - } - - internal ulong ReadU64() - { - var lo = (ulong)ReadU32(); - var hi = (ulong)ReadU32(); - return lo | (hi << 32); - } - - internal long ReadI64() - { - return (long)ReadU64(); - } - - internal float ReadF32() - { - return BitConverter.Int32BitsToSingle(ReadI32()); - } - - internal double ReadF64() - { - return BitConverter.Int64BitsToDouble(ReadI64()); - } - - internal int ReadLen() - { - var len = ReadU32(); - if (len > (uint)(_data.Length - _pos)) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: length prefix exceeds remaining bytes"); - } - return (int)len; - } - - internal bool ReadOptionFlag() - { - return ReadBool(); - } - - internal string ReadString() - { - var len = ReadLen(); - string s; - try - { - s = Utf8Strict.GetString(_data, _pos, len); - } - catch (ArgumentException) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: string is not valid UTF-8"); - } - _pos += len; - return s; - } - - internal byte[] ReadBytes() - { - var len = ReadLen(); - var outBuf = new byte[len]; - Array.Copy(_data, _pos, outBuf, 0, len); - _pos += len; - return outBuf; - } - - internal void ExpectEnd() - { - if (_pos != _data.Length) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: trailing bytes"); - } - } - } - - public sealed class TaskResult - { - public long Id { get; } - - public string Value { get; } - - public bool Success { get; } - - public TaskResult(long id, string value, bool success) - { - Id = id; - Value = value; - Success = success; - } - - internal void WriteTo(WeaveFFIBufferWriter writer) - { - writer.WriteI64(Id); - writer.WriteString(Value); - writer.WriteBool(Success); - } - - internal static TaskResult ReadFrom(WeaveFFIBufferReader reader) - { - var fId = reader.ReadI64(); - var fValue = reader.ReadString(); - var fSuccess = reader.ReadBool(); - return new TaskResult(fId, fValue, fSuccess); - } - } - - internal static class NativeMethods - { - private const string LibName = "weaveffi"; - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_free_string(IntPtr ptr); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_free_bytes(IntPtr ptr, UIntPtr len); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_clear(ref WeaveFFIError err); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_free(IntPtr err); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - internal delegate void AsyncCb_weaveffi_tasks_run_task(IntPtr context, IntPtr err, IntPtr result, UIntPtr resultLen); - - [DllImport(LibName, EntryPoint = "weaveffi_tasks_run_task_async", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_tasks_run_task_async(IntPtr name, AsyncCb_weaveffi_tasks_run_task callback, IntPtr context); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - internal delegate void AsyncCb_weaveffi_tasks_run_batch(IntPtr context, IntPtr err, IntPtr result, UIntPtr resultLen); - - [DllImport(LibName, EntryPoint = "weaveffi_tasks_run_batch_async", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_tasks_run_batch_async(IntPtr names_ptr, UIntPtr names_len, AsyncCb_weaveffi_tasks_run_batch callback, IntPtr context); - - [DllImport(LibName, EntryPoint = "weaveffi_tasks_cancel_task", CallingConvention = CallingConvention.Cdecl)] - internal static extern byte weaveffi_tasks_cancel_task(long id, ref WeaveFFIError err); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - internal delegate void AsyncCb_weaveffi_tasks_run_n_tasks(IntPtr context, IntPtr err, int result); - - [DllImport(LibName, EntryPoint = "weaveffi_tasks_run_n_tasks_async", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_tasks_run_n_tasks_async(int n, AsyncCb_weaveffi_tasks_run_n_tasks callback, IntPtr context); - - [DllImport(LibName, EntryPoint = "weaveffi_tasks_active_callbacks", CallingConvention = CallingConvention.Cdecl)] - internal static extern long weaveffi_tasks_active_callbacks(ref WeaveFFIError err); - - } - - public static class Tasks - { - /// Thrown when the call reports a TaskError code. - public static async Task RunTask(string name) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - NativeMethods.AsyncCb_weaveffi_tasks_run_task callback = (context, err, result, resultLen) => - { - try - { - if (err != IntPtr.Zero) - { - var wErr = Marshal.PtrToStructure(err); - if (wErr.Code != 0) - { - var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? ""; - var payload = WeaveFFIError.CopyPayload(wErr); - NativeMethods.weaveffi_error_free(err); - tcs.SetException(TaskException.FromCode(wErr.Code, msg, payload)); - return; - } - } - var resultBuf = new byte[(int)resultLen]; - if (result != IntPtr.Zero && (int)resultLen > 0) Marshal.Copy(result, resultBuf, 0, (int)resultLen); - if (result != IntPtr.Zero) NativeMethods.weaveffi_free_bytes(result, resultLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var value = TaskResult.ReadFrom(valueReader); - valueReader.ExpectEnd(); - tcs.SetResult(value); - } - finally - { - if (context != IntPtr.Zero) - { - GCHandle.FromIntPtr(context).Free(); - } - } - }; - var gcHandle = GCHandle.Alloc(callback, GCHandleType.Normal); - var ctx = GCHandle.ToIntPtr(gcHandle); - var namePtr = Marshal.StringToCoTaskMemUTF8(name); - try - { - try - { - NativeMethods.weaveffi_tasks_run_task_async(namePtr, callback, ctx); - } - catch - { - if (gcHandle.IsAllocated) gcHandle.Free(); - throw; - } - } - finally - { - Marshal.FreeCoTaskMem(namePtr); - } - return await tcs.Task; - } - - public static async Task RunBatch(string[] names) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - NativeMethods.AsyncCb_weaveffi_tasks_run_batch callback = (context, err, result, resultLen) => - { - try - { - if (err != IntPtr.Zero) - { - var wErr = Marshal.PtrToStructure(err); - if (wErr.Code != 0) - { - var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? ""; - NativeMethods.weaveffi_error_free(err); - tcs.SetException(new WeaveFFIException(wErr.Code, msg)); - return; - } - } - var resultBuf = new byte[(int)resultLen]; - if (result != IntPtr.Zero && (int)resultLen > 0) Marshal.Copy(result, resultBuf, 0, (int)resultLen); - if (result != IntPtr.Zero) NativeMethods.weaveffi_free_bytes(result, resultLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var valueCount = valueReader.ReadLen(); - var value = new TaskResult[valueCount]; - for (int i0 = 0; i0 < valueCount; i0++) - { - var valueItem = TaskResult.ReadFrom(valueReader); - value[i0] = valueItem; - } - valueReader.ExpectEnd(); - tcs.SetResult(value); - } - finally - { - if (context != IntPtr.Zero) - { - GCHandle.FromIntPtr(context).Free(); - } - } - }; - var gcHandle = GCHandle.Alloc(callback, GCHandleType.Normal); - var ctx = GCHandle.ToIntPtr(gcHandle); - var namesWriter = new WeaveFFIBufferWriter(); - namesWriter.WriteLen(names.Length); - foreach (var item0 in names) - { - namesWriter.WriteString(item0); - } - var namesBuf = namesWriter.ToArray(); - var namesPin = GCHandle.Alloc(namesBuf, GCHandleType.Pinned); - try - { - try - { - NativeMethods.weaveffi_tasks_run_batch_async(namesPin.AddrOfPinnedObject(), (UIntPtr)namesBuf.Length, callback, ctx); - } - catch - { - if (gcHandle.IsAllocated) gcHandle.Free(); - throw; - } - } - finally - { - namesPin.Free(); - } - return await tcs.Task; - } - - public static bool CancelTask(long id) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_tasks_cancel_task(id, ref err); - WeaveFFIError.Check(err); - return result != 0; - } - - public static async Task RunNTasks(int n) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - NativeMethods.AsyncCb_weaveffi_tasks_run_n_tasks callback = (context, err, result) => - { - try - { - if (err != IntPtr.Zero) - { - var wErr = Marshal.PtrToStructure(err); - if (wErr.Code != 0) - { - var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? ""; - NativeMethods.weaveffi_error_free(err); - tcs.SetException(new WeaveFFIException(wErr.Code, msg)); - return; - } - } - tcs.SetResult(result); - } - finally - { - if (context != IntPtr.Zero) - { - GCHandle.FromIntPtr(context).Free(); - } - } - }; - var gcHandle = GCHandle.Alloc(callback, GCHandleType.Normal); - var ctx = GCHandle.ToIntPtr(gcHandle); - try - { - NativeMethods.weaveffi_tasks_run_n_tasks_async(n, callback, ctx); - } - catch - { - if (gcHandle.IsAllocated) gcHandle.Free(); - throw; - } - return await tcs.Task; - } - - public static long ActiveCallbacks() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_tasks_active_callbacks(ref err); - WeaveFFIError.Check(err); - return result; - } - - } - -} - -// END WeaveFFI.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_calculator__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_calculator__README_md.snap deleted file mode 100644 index 96a3a5e8..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_calculator__README_md.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI (.NET) - -Auto-generated P/Invoke bindings for the WeaveFFI native library. - -## Build - -```bash -dotnet build -``` - -## Pack - -```bash -dotnet pack -``` - -The resulting `.nupkg` will be in `bin/Debug/` (or `bin/Release/` with `-c Release`). - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_calculator__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_calculator__WeaveFFI_cs.snap deleted file mode 100644 index 1ac303ba..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_calculator__WeaveFFI_cs.snap +++ /dev/null @@ -1,463 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace WeaveFFI -{ - public class WeaveFFIException : Exception - { - public int Code { get; } - - public WeaveFFIException(int code, string message) : base(message) - { - Code = code; - } - } - - /// Typed exception for the CalcError error domain (module calculator). - public class CalcException : WeaveFFIException - { - /// division by zero - public const int DivisionByZero = 1; - - public CalcException(int code, string message) : base(code, message) - { - } - - /// Wraps a raw error slot in the typed exception, falling - /// back to for unknown codes. Codes - /// declaring payload fields decode them into Data. - internal static WeaveFFIException FromCode(int code, string message, byte[]? payload) - { - switch (code) - { - case DivisionByZero: - return new CalcException(code, string.IsNullOrEmpty(message) ? "division by zero" : message); - default: - return new WeaveFFIException(code, message); - } - } - } - - [StructLayout(LayoutKind.Sequential)] - internal struct WeaveFFIError - { - public int Code; - public IntPtr Message; - public IntPtr PayloadPtr; - public UIntPtr PayloadLen; - - internal static byte[]? CopyPayload(WeaveFFIError err) - { - if (err.PayloadPtr == IntPtr.Zero || (int)err.PayloadLen == 0) - { - return null; - } - var payload = new byte[(int)err.PayloadLen]; - Marshal.Copy(err.PayloadPtr, payload, 0, (int)err.PayloadLen); - return payload; - } - - internal static void Check(WeaveFFIError err) - { - if (err.Code != 0) - { - var code = err.Code; - var msg = Marshal.PtrToStringUTF8(err.Message) ?? ""; - NativeMethods.weaveffi_error_clear(ref err); - throw new WeaveFFIException(code, msg); - } - } - - internal static void CheckCalc(WeaveFFIError err) - { - if (err.Code != 0) - { - var code = err.Code; - var msg = Marshal.PtrToStringUTF8(err.Message) ?? ""; - var payload = CopyPayload(err); - NativeMethods.weaveffi_error_clear(ref err); - throw CalcException.FromCode(code, msg, payload); - } - } - } - - internal static class WeaveFFIHelpers - { - internal static IntPtr StringToPtr(string? s) - { - return s == null ? IntPtr.Zero : Marshal.StringToCoTaskMemUTF8(s); - } - - internal static string? PtrToString(IntPtr ptr) - { - return ptr == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(ptr); - } - - internal static void FreePtr(IntPtr ptr) - { - Marshal.FreeCoTaskMem(ptr); - } - } - - /// Serializes values into the WeaveFFI value-buffer wire - /// format (little-endian, packed). - internal sealed class WeaveFFIBufferWriter - { - private byte[] _buf = new byte[64]; - private int _len; - - private void Ensure(int extra) - { - if (_len + extra <= _buf.Length) - { - return; - } - var size = _buf.Length * 2; - while (size < _len + extra) - { - size *= 2; - } - Array.Resize(ref _buf, size); - } - - internal void WriteBool(bool v) - { - Ensure(1); - _buf[_len++] = v ? (byte)1 : (byte)0; - } - - internal void WriteI8(sbyte v) - { - Ensure(1); - _buf[_len++] = (byte)v; - } - - internal void WriteU8(byte v) - { - Ensure(1); - _buf[_len++] = v; - } - - internal void WriteU16(ushort v) - { - Ensure(2); - _buf[_len++] = (byte)v; - _buf[_len++] = (byte)(v >> 8); - } - - internal void WriteI16(short v) - { - WriteU16((ushort)v); - } - - internal void WriteU32(uint v) - { - Ensure(4); - _buf[_len++] = (byte)v; - _buf[_len++] = (byte)(v >> 8); - _buf[_len++] = (byte)(v >> 16); - _buf[_len++] = (byte)(v >> 24); - } - - internal void WriteI32(int v) - { - WriteU32((uint)v); - } - - internal void WriteU64(ulong v) - { - WriteU32((uint)v); - WriteU32((uint)(v >> 32)); - } - - internal void WriteI64(long v) - { - WriteU64((ulong)v); - } - - internal void WriteF32(float v) - { - WriteU32((uint)BitConverter.SingleToInt32Bits(v)); - } - - internal void WriteF64(double v) - { - WriteU64((ulong)BitConverter.DoubleToInt64Bits(v)); - } - - internal void WriteLen(int len) - { - WriteU32((uint)len); - } - - internal void WriteOptionFlag(bool present) - { - WriteBool(present); - } - - internal void WriteString(string v) - { - var bytes = System.Text.Encoding.UTF8.GetBytes(v); - WriteLen(bytes.Length); - Ensure(bytes.Length); - Array.Copy(bytes, 0, _buf, _len, bytes.Length); - _len += bytes.Length; - } - - internal void WriteBytes(byte[] v) - { - WriteLen(v.Length); - Ensure(v.Length); - Array.Copy(v, 0, _buf, _len, v.Length); - _len += v.Length; - } - - internal byte[] ToArray() - { - var outBuf = new byte[_len]; - Array.Copy(_buf, outBuf, _len); - return outBuf; - } - } - - /// Decodes values from the WeaveFFI value-buffer wire format. - /// A malformed buffer indicates a producer/consumer contract violation and - /// throws . - internal sealed class WeaveFFIBufferReader - { - private static readonly System.Text.Encoding Utf8Strict = - new System.Text.UTF8Encoding(false, true); - - private readonly byte[] _data; - private int _pos; - - internal WeaveFFIBufferReader(byte[] data) - { - _data = data; - } - - private void Require(int n) - { - if (_data.Length - _pos < n) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: buffer exhausted"); - } - } - - internal bool ReadBool() - { - Require(1); - var b = _data[_pos++]; - if (b > 1) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: invalid bool byte"); - } - return b == 1; - } - - internal sbyte ReadI8() - { - Require(1); - return (sbyte)_data[_pos++]; - } - - internal byte ReadU8() - { - Require(1); - return _data[_pos++]; - } - - internal ushort ReadU16() - { - Require(2); - var v = (ushort)(_data[_pos] | (_data[_pos + 1] << 8)); - _pos += 2; - return v; - } - - internal short ReadI16() - { - return (short)ReadU16(); - } - - internal uint ReadU32() - { - Require(4); - var v = (uint)_data[_pos] - | ((uint)_data[_pos + 1] << 8) - | ((uint)_data[_pos + 2] << 16) - | ((uint)_data[_pos + 3] << 24); - _pos += 4; - return v; - } - - internal int ReadI32() - { - return (int)ReadU32(); - } - - internal ulong ReadU64() - { - var lo = (ulong)ReadU32(); - var hi = (ulong)ReadU32(); - return lo | (hi << 32); - } - - internal long ReadI64() - { - return (long)ReadU64(); - } - - internal float ReadF32() - { - return BitConverter.Int32BitsToSingle(ReadI32()); - } - - internal double ReadF64() - { - return BitConverter.Int64BitsToDouble(ReadI64()); - } - - internal int ReadLen() - { - var len = ReadU32(); - if (len > (uint)(_data.Length - _pos)) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: length prefix exceeds remaining bytes"); - } - return (int)len; - } - - internal bool ReadOptionFlag() - { - return ReadBool(); - } - - internal string ReadString() - { - var len = ReadLen(); - string s; - try - { - s = Utf8Strict.GetString(_data, _pos, len); - } - catch (ArgumentException) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: string is not valid UTF-8"); - } - _pos += len; - return s; - } - - internal byte[] ReadBytes() - { - var len = ReadLen(); - var outBuf = new byte[len]; - Array.Copy(_data, _pos, outBuf, 0, len); - _pos += len; - return outBuf; - } - - internal void ExpectEnd() - { - if (_pos != _data.Length) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: trailing bytes"); - } - } - } - - internal static class NativeMethods - { - private const string LibName = "weaveffi"; - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_free_string(IntPtr ptr); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_free_bytes(IntPtr ptr, UIntPtr len); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_clear(ref WeaveFFIError err); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_free(IntPtr err); - - [DllImport(LibName, EntryPoint = "weaveffi_calculator_add", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_calculator_add(int a, int b, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_calculator_mul", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_calculator_mul(int a, int b, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_calculator_div", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_calculator_div(int a, int b, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_calculator_echo", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_calculator_echo(IntPtr s, ref WeaveFFIError err); - - } - - public static class Calculator - { - /// Add two integers - public static int Add(int a, int b) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_calculator_add(a, b, ref err); - WeaveFFIError.Check(err); - return result; - } - - /// Multiply two integers - public static int Mul(int a, int b) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_calculator_mul(a, b, ref err); - WeaveFFIError.Check(err); - return result; - } - - /// Divide two integers, failing on a zero divisor - /// Thrown when the call reports a CalcError code. - public static int Div(int a, int b) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_calculator_div(a, b, ref err); - WeaveFFIError.CheckCalc(err); - return result; - } - - /// Echo a string back - public static string Echo(string s) - { - var err = new WeaveFFIError(); - var sPtr = Marshal.StringToCoTaskMemUTF8(s); - try - { - var result = NativeMethods.weaveffi_calculator_echo(sPtr, ref err); - WeaveFFIError.Check(err); - var str = Marshal.PtrToStringUTF8(result); - NativeMethods.weaveffi_free_string(result); - return str ?? ""; - } - finally - { - Marshal.FreeCoTaskMem(sPtr); - } - } - - } - -} - -// END WeaveFFI.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_calculator__WeaveFFI_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_calculator__WeaveFFI_csproj.snap deleted file mode 100644 index 97f37904..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_calculator__WeaveFFI_csproj.snap +++ /dev/null @@ -1,20 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - - - - - net8.0 - WeaveFFI - 0.1.0 - WeaveFFI bindings generated by WeaveFFI - true - - - - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_calculator__WeaveFFI_nuspec.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_calculator__WeaveFFI_nuspec.snap deleted file mode 100644 index 55db5121..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_calculator__WeaveFFI_nuspec.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - - - - - WeaveFFI - 0.1.0 - WeaveFFI Contributors - WeaveFFI bindings generated by WeaveFFI - MIT - https://github.com/weavefoundry/weaveffi - ffi interop native pinvoke - - - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_contacts__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_contacts__README_md.snap deleted file mode 100644 index 96a3a5e8..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_contacts__README_md.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI (.NET) - -Auto-generated P/Invoke bindings for the WeaveFFI native library. - -## Build - -```bash -dotnet build -``` - -## Pack - -```bash -dotnet pack -``` - -The resulting `.nupkg` will be in `bin/Debug/` (or `bin/Release/` with `-c Release`). - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_contacts__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_contacts__WeaveFFI_cs.snap deleted file mode 100644 index e9f1f435..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_contacts__WeaveFFI_cs.snap +++ /dev/null @@ -1,616 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace WeaveFFI -{ - public class WeaveFFIException : Exception - { - public int Code { get; } - - public WeaveFFIException(int code, string message) : base(message) - { - Code = code; - } - } - - /// Typed exception for the ContactsError error domain (module contacts). - public class ContactsException : WeaveFFIException - { - /// name must not be empty - public const int InvalidName = 1; - /// contact not found - public const int NotFound = 2; - - public ContactsException(int code, string message) : base(code, message) - { - } - - /// Wraps a raw error slot in the typed exception, falling - /// back to for unknown codes. Codes - /// declaring payload fields decode them into Data. - internal static WeaveFFIException FromCode(int code, string message, byte[]? payload) - { - switch (code) - { - case InvalidName: - return new ContactsException(code, string.IsNullOrEmpty(message) ? "name must not be empty" : message); - case NotFound: - return new ContactsException(code, string.IsNullOrEmpty(message) ? "contact not found" : message); - default: - return new WeaveFFIException(code, message); - } - } - } - - [StructLayout(LayoutKind.Sequential)] - internal struct WeaveFFIError - { - public int Code; - public IntPtr Message; - public IntPtr PayloadPtr; - public UIntPtr PayloadLen; - - internal static byte[]? CopyPayload(WeaveFFIError err) - { - if (err.PayloadPtr == IntPtr.Zero || (int)err.PayloadLen == 0) - { - return null; - } - var payload = new byte[(int)err.PayloadLen]; - Marshal.Copy(err.PayloadPtr, payload, 0, (int)err.PayloadLen); - return payload; - } - - internal static void Check(WeaveFFIError err) - { - if (err.Code != 0) - { - var code = err.Code; - var msg = Marshal.PtrToStringUTF8(err.Message) ?? ""; - NativeMethods.weaveffi_error_clear(ref err); - throw new WeaveFFIException(code, msg); - } - } - - internal static void CheckContacts(WeaveFFIError err) - { - if (err.Code != 0) - { - var code = err.Code; - var msg = Marshal.PtrToStringUTF8(err.Message) ?? ""; - var payload = CopyPayload(err); - NativeMethods.weaveffi_error_clear(ref err); - throw ContactsException.FromCode(code, msg, payload); - } - } - } - - internal static class WeaveFFIHelpers - { - internal static IntPtr StringToPtr(string? s) - { - return s == null ? IntPtr.Zero : Marshal.StringToCoTaskMemUTF8(s); - } - - internal static string? PtrToString(IntPtr ptr) - { - return ptr == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(ptr); - } - - internal static void FreePtr(IntPtr ptr) - { - Marshal.FreeCoTaskMem(ptr); - } - } - - /// Serializes values into the WeaveFFI value-buffer wire - /// format (little-endian, packed). - internal sealed class WeaveFFIBufferWriter - { - private byte[] _buf = new byte[64]; - private int _len; - - private void Ensure(int extra) - { - if (_len + extra <= _buf.Length) - { - return; - } - var size = _buf.Length * 2; - while (size < _len + extra) - { - size *= 2; - } - Array.Resize(ref _buf, size); - } - - internal void WriteBool(bool v) - { - Ensure(1); - _buf[_len++] = v ? (byte)1 : (byte)0; - } - - internal void WriteI8(sbyte v) - { - Ensure(1); - _buf[_len++] = (byte)v; - } - - internal void WriteU8(byte v) - { - Ensure(1); - _buf[_len++] = v; - } - - internal void WriteU16(ushort v) - { - Ensure(2); - _buf[_len++] = (byte)v; - _buf[_len++] = (byte)(v >> 8); - } - - internal void WriteI16(short v) - { - WriteU16((ushort)v); - } - - internal void WriteU32(uint v) - { - Ensure(4); - _buf[_len++] = (byte)v; - _buf[_len++] = (byte)(v >> 8); - _buf[_len++] = (byte)(v >> 16); - _buf[_len++] = (byte)(v >> 24); - } - - internal void WriteI32(int v) - { - WriteU32((uint)v); - } - - internal void WriteU64(ulong v) - { - WriteU32((uint)v); - WriteU32((uint)(v >> 32)); - } - - internal void WriteI64(long v) - { - WriteU64((ulong)v); - } - - internal void WriteF32(float v) - { - WriteU32((uint)BitConverter.SingleToInt32Bits(v)); - } - - internal void WriteF64(double v) - { - WriteU64((ulong)BitConverter.DoubleToInt64Bits(v)); - } - - internal void WriteLen(int len) - { - WriteU32((uint)len); - } - - internal void WriteOptionFlag(bool present) - { - WriteBool(present); - } - - internal void WriteString(string v) - { - var bytes = System.Text.Encoding.UTF8.GetBytes(v); - WriteLen(bytes.Length); - Ensure(bytes.Length); - Array.Copy(bytes, 0, _buf, _len, bytes.Length); - _len += bytes.Length; - } - - internal void WriteBytes(byte[] v) - { - WriteLen(v.Length); - Ensure(v.Length); - Array.Copy(v, 0, _buf, _len, v.Length); - _len += v.Length; - } - - internal byte[] ToArray() - { - var outBuf = new byte[_len]; - Array.Copy(_buf, outBuf, _len); - return outBuf; - } - } - - /// Decodes values from the WeaveFFI value-buffer wire format. - /// A malformed buffer indicates a producer/consumer contract violation and - /// throws . - internal sealed class WeaveFFIBufferReader - { - private static readonly System.Text.Encoding Utf8Strict = - new System.Text.UTF8Encoding(false, true); - - private readonly byte[] _data; - private int _pos; - - internal WeaveFFIBufferReader(byte[] data) - { - _data = data; - } - - private void Require(int n) - { - if (_data.Length - _pos < n) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: buffer exhausted"); - } - } - - internal bool ReadBool() - { - Require(1); - var b = _data[_pos++]; - if (b > 1) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: invalid bool byte"); - } - return b == 1; - } - - internal sbyte ReadI8() - { - Require(1); - return (sbyte)_data[_pos++]; - } - - internal byte ReadU8() - { - Require(1); - return _data[_pos++]; - } - - internal ushort ReadU16() - { - Require(2); - var v = (ushort)(_data[_pos] | (_data[_pos + 1] << 8)); - _pos += 2; - return v; - } - - internal short ReadI16() - { - return (short)ReadU16(); - } - - internal uint ReadU32() - { - Require(4); - var v = (uint)_data[_pos] - | ((uint)_data[_pos + 1] << 8) - | ((uint)_data[_pos + 2] << 16) - | ((uint)_data[_pos + 3] << 24); - _pos += 4; - return v; - } - - internal int ReadI32() - { - return (int)ReadU32(); - } - - internal ulong ReadU64() - { - var lo = (ulong)ReadU32(); - var hi = (ulong)ReadU32(); - return lo | (hi << 32); - } - - internal long ReadI64() - { - return (long)ReadU64(); - } - - internal float ReadF32() - { - return BitConverter.Int32BitsToSingle(ReadI32()); - } - - internal double ReadF64() - { - return BitConverter.Int64BitsToDouble(ReadI64()); - } - - internal int ReadLen() - { - var len = ReadU32(); - if (len > (uint)(_data.Length - _pos)) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: length prefix exceeds remaining bytes"); - } - return (int)len; - } - - internal bool ReadOptionFlag() - { - return ReadBool(); - } - - internal string ReadString() - { - var len = ReadLen(); - string s; - try - { - s = Utf8Strict.GetString(_data, _pos, len); - } - catch (ArgumentException) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: string is not valid UTF-8"); - } - _pos += len; - return s; - } - - internal byte[] ReadBytes() - { - var len = ReadLen(); - var outBuf = new byte[len]; - Array.Copy(_data, _pos, outBuf, 0, len); - _pos += len; - return outBuf; - } - - internal void ExpectEnd() - { - if (_pos != _data.Length) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: trailing bytes"); - } - } - } - - public enum ContactType - { - Personal = 0, - Work = 1, - Other = 2, - } - - public sealed class Contact - { - public long Id { get; } - - public string FirstName { get; } - - public string LastName { get; } - - public string? Email { get; } - - public ContactType ContactType { get; } - - public Contact(long id, string firstName, string lastName, string? email, ContactType contactType) - { - Id = id; - FirstName = firstName; - LastName = lastName; - Email = email; - ContactType = contactType; - } - - internal void WriteTo(WeaveFFIBufferWriter writer) - { - writer.WriteI64(Id); - writer.WriteString(FirstName); - writer.WriteString(LastName); - if (Email != null) - { - writer.WriteOptionFlag(true); - writer.WriteString(Email!); - } - else - { - writer.WriteOptionFlag(false); - } - writer.WriteI32((int)ContactType); - } - - internal static Contact ReadFrom(WeaveFFIBufferReader reader) - { - var fId = reader.ReadI64(); - var fFirstName = reader.ReadString(); - var fLastName = reader.ReadString(); - string? fEmail = null; - if (reader.ReadOptionFlag()) - { - var fEmailValue = reader.ReadString(); - fEmail = fEmailValue; - } - var fContactType = (ContactType)reader.ReadI32(); - return new Contact(fId, fFirstName, fLastName, fEmail, fContactType); - } - } - - /// An in-memory address book owning its contacts - public class ContactBook : IDisposable - { - private IntPtr _handle; - private bool _disposed; - - internal ContactBook(IntPtr handle) - { - _handle = handle; - } - - internal IntPtr Handle => _handle; - - public ContactBook() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_contacts_ContactBook_new(ref err); - WeaveFFIError.Check(err); - _handle = result; - } - - /// Thrown when the call reports a ContactsError code. - public Contact Add(string firstName, string lastName, string? email, ContactType contactType) - { - var err = new WeaveFFIError(); - var firstNamePtr = Marshal.StringToCoTaskMemUTF8(firstName); - var lastNamePtr = Marshal.StringToCoTaskMemUTF8(lastName); - var emailWriter = new WeaveFFIBufferWriter(); - if (email != null) - { - emailWriter.WriteOptionFlag(true); - emailWriter.WriteString(email!); - } - else - { - emailWriter.WriteOptionFlag(false); - } - var emailBuf = emailWriter.ToArray(); - var emailPin = GCHandle.Alloc(emailBuf, GCHandleType.Pinned); - try - { - var result = NativeMethods.weaveffi_contacts_ContactBook_add(_handle, firstNamePtr, lastNamePtr, emailPin.AddrOfPinnedObject(), (UIntPtr)emailBuf.Length, (int)contactType, out var outLen, ref err); - WeaveFFIError.CheckContacts(err); - var resultBuf = new byte[(int)outLen]; - if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); - NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var value = Contact.ReadFrom(valueReader); - valueReader.ExpectEnd(); - return value; - } - finally - { - Marshal.FreeCoTaskMem(firstNamePtr); - Marshal.FreeCoTaskMem(lastNamePtr); - emailPin.Free(); - } - } - - /// Thrown when the call reports a ContactsError code. - public Contact Get(long id) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_contacts_ContactBook_get(_handle, id, out var outLen, ref err); - WeaveFFIError.CheckContacts(err); - var resultBuf = new byte[(int)outLen]; - if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); - NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var value = Contact.ReadFrom(valueReader); - valueReader.ExpectEnd(); - return value; - } - - public Contact[] List() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_contacts_ContactBook_list(_handle, out var outLen, ref err); - WeaveFFIError.Check(err); - var resultBuf = new byte[(int)outLen]; - if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); - NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var valueCount = valueReader.ReadLen(); - var value = new Contact[valueCount]; - for (int i0 = 0; i0 < valueCount; i0++) - { - var valueItem = Contact.ReadFrom(valueReader); - value[i0] = valueItem; - } - valueReader.ExpectEnd(); - return value; - } - - public bool Remove(long id) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_contacts_ContactBook_remove(_handle, id, ref err); - WeaveFFIError.Check(err); - return result != 0; - } - - public int Count() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_contacts_ContactBook_count(_handle, ref err); - WeaveFFIError.Check(err); - return result; - } - - public void Dispose() - { - if (!_disposed) - { - NativeMethods.weaveffi_contacts_ContactBook_destroy(_handle); - _disposed = true; - } - GC.SuppressFinalize(this); - } - - ~ContactBook() - { - Dispose(); - } - } - - internal static class NativeMethods - { - private const string LibName = "weaveffi"; - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_free_string(IntPtr ptr); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_free_bytes(IntPtr ptr, UIntPtr len); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_clear(ref WeaveFFIError err); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_free(IntPtr err); - - [DllImport(LibName, EntryPoint = "weaveffi_contacts_ContactBook_destroy", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_contacts_ContactBook_destroy(IntPtr self); - - [DllImport(LibName, EntryPoint = "weaveffi_contacts_ContactBook_new", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_contacts_ContactBook_new(ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_contacts_ContactBook_add", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_contacts_ContactBook_add(IntPtr self, IntPtr first_name, IntPtr last_name, IntPtr email_ptr, UIntPtr email_len, int contact_type, out UIntPtr out_len, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_contacts_ContactBook_get", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_contacts_ContactBook_get(IntPtr self, long id, out UIntPtr out_len, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_contacts_ContactBook_list", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_contacts_ContactBook_list(IntPtr self, out UIntPtr out_len, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_contacts_ContactBook_remove", CallingConvention = CallingConvention.Cdecl)] - internal static extern byte weaveffi_contacts_ContactBook_remove(IntPtr self, long id, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_contacts_ContactBook_count", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_contacts_ContactBook_count(IntPtr self, ref WeaveFFIError err); - - } - - public static class Contacts - { - } - -} - -// END WeaveFFI.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_contacts__WeaveFFI_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_contacts__WeaveFFI_csproj.snap deleted file mode 100644 index 97f37904..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_contacts__WeaveFFI_csproj.snap +++ /dev/null @@ -1,20 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - - - - - net8.0 - WeaveFFI - 0.1.0 - WeaveFFI bindings generated by WeaveFFI - true - - - - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_contacts__WeaveFFI_nuspec.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_contacts__WeaveFFI_nuspec.snap deleted file mode 100644 index 55db5121..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_contacts__WeaveFFI_nuspec.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - - - - - WeaveFFI - 0.1.0 - WeaveFFI Contributors - WeaveFFI bindings generated by WeaveFFI - MIT - https://github.com/weavefoundry/weaveffi - ffi interop native pinvoke - - - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__WeaveFFI_cs.snap index dbf576e3..f870da95 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__WeaveFFI_cs.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_docs_everywhere__WeaveFFI_cs.snap @@ -510,10 +510,10 @@ namespace WeaveFFI var resultBuf = new byte[(int)outLen]; if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var value = Document.ReadFrom(valueReader); - valueReader.ExpectEnd(); - return value; + var decodedReader = new WeaveFFIBufferReader(resultBuf); + var decoded = Document.ReadFrom(decodedReader); + decodedReader.ExpectEnd(); + return decoded; } /// The number of documents currently held @@ -554,6 +554,34 @@ namespace WeaveFFI { private const string LibName = "weaveffi"; + // The ABI revision these bindings were generated against. + internal const uint AbiVersion = 1; + + // Runs before the first P/Invoke through this class, so a producer built + // for a different ABI revision fails loudly instead of misreading the + // error struct or a value buffer later. + static NativeMethods() + { + uint found; + try + { + found = weaveffi_abi_version(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded WeaveFFI library predates ABI versioning (these bindings expect ABI revision {AbiVersion})", e); + } + if (found != AbiVersion) + { + throw new InvalidOperationException( + $"WeaveFFI ABI mismatch: these bindings expect revision {AbiVersion} but the loaded library reports revision {found}"); + } + } + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern uint weaveffi_abi_version(); + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] internal static extern void weaveffi_free_string(IntPtr ptr); @@ -669,10 +697,10 @@ namespace WeaveFFI var resultBuf = new byte[(int)outLen]; if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var value = Document.ReadFrom(valueReader); - valueReader.ExpectEnd(); - return value; + var decodedReader = new WeaveFFIBufferReader(resultBuf); + var decoded = Document.ReadFrom(decodedReader); + decodedReader.ExpectEnd(); + return decoded; } finally { @@ -691,15 +719,15 @@ namespace WeaveFFI var resultBuf = new byte[(int)outLen]; if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - Document? value = null; - if (valueReader.ReadOptionFlag()) + var decodedReader = new WeaveFFIBufferReader(resultBuf); + Document? decoded = null; + if (decodedReader.ReadOptionFlag()) { - var valueValue = Document.ReadFrom(valueReader); - value = valueValue; + var decodedValue = Document.ReadFrom(decodedReader); + decoded = decodedValue; } - valueReader.ExpectEnd(); - return value; + decodedReader.ExpectEnd(); + return decoded; } /// Publish a document in the background diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_async_demo__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__README_md.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/dotnet_async_demo__README_md.snap rename to crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__README_md.snap diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__WeaveFFI_cs.snap new file mode 100644 index 00000000..419ba86f --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__WeaveFFI_cs.snap @@ -0,0 +1,2264 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from weaveffi.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate weaveffi.yml -o + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace WeaveFFI +{ + public class WeaveFFIException : Exception + { + public int Code { get; } + + public WeaveFFIException(int code, string message) : base(message) + { + Code = code; + } + } + + /// Typed exception for the EdgeErrorsError error domain (module edge.cases). + public class EdgeErrorsException : WeaveFFIException + { + /// Resource is busy + public const int Busy = 7; + + public EdgeErrorsException(int code, string message) : base(code, message) + { + } + + /// Wraps a raw error slot in the typed exception, falling + /// back to for unknown codes. Codes + /// declaring payload fields decode them into Data. + internal static WeaveFFIException FromCode(int code, string message, byte[]? payload) + { + switch (code) + { + case Busy: + return new EdgeErrorsException(code, string.IsNullOrEmpty(message) ? "Resource is busy" : message); + default: + return new WeaveFFIException(code, message); + } + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct WeaveFFIError + { + public int Code; + public IntPtr Message; + public IntPtr PayloadPtr; + public UIntPtr PayloadLen; + + internal static byte[]? CopyPayload(WeaveFFIError err) + { + if (err.PayloadPtr == IntPtr.Zero || (int)err.PayloadLen == 0) + { + return null; + } + var payload = new byte[(int)err.PayloadLen]; + Marshal.Copy(err.PayloadPtr, payload, 0, (int)err.PayloadLen); + return payload; + } + + internal static void Check(WeaveFFIError err) + { + if (err.Code != 0) + { + var code = err.Code; + var msg = Marshal.PtrToStringUTF8(err.Message) ?? ""; + NativeMethods.weaveffi_error_clear(ref err); + throw new WeaveFFIException(code, msg); + } + } + + internal static void CheckEdgeErrors(WeaveFFIError err) + { + if (err.Code != 0) + { + var code = err.Code; + var msg = Marshal.PtrToStringUTF8(err.Message) ?? ""; + var payload = CopyPayload(err); + NativeMethods.weaveffi_error_clear(ref err); + throw EdgeErrorsException.FromCode(code, msg, payload); + } + } + } + + internal static class WeaveFFIHelpers + { + internal static IntPtr StringToPtr(string? s) + { + return s == null ? IntPtr.Zero : Marshal.StringToCoTaskMemUTF8(s); + } + + internal static string? PtrToString(IntPtr ptr) + { + return ptr == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(ptr); + } + + internal static void FreePtr(IntPtr ptr) + { + Marshal.FreeCoTaskMem(ptr); + } + } + + /// Serializes values into the WeaveFFI value-buffer wire + /// format (little-endian, packed). + internal sealed class WeaveFFIBufferWriter + { + private byte[] _buf = new byte[64]; + private int _len; + + private void Ensure(int extra) + { + if (_len + extra <= _buf.Length) + { + return; + } + var size = _buf.Length * 2; + while (size < _len + extra) + { + size *= 2; + } + Array.Resize(ref _buf, size); + } + + internal void WriteBool(bool v) + { + Ensure(1); + _buf[_len++] = v ? (byte)1 : (byte)0; + } + + internal void WriteI8(sbyte v) + { + Ensure(1); + _buf[_len++] = (byte)v; + } + + internal void WriteU8(byte v) + { + Ensure(1); + _buf[_len++] = v; + } + + internal void WriteU16(ushort v) + { + Ensure(2); + _buf[_len++] = (byte)v; + _buf[_len++] = (byte)(v >> 8); + } + + internal void WriteI16(short v) + { + WriteU16((ushort)v); + } + + internal void WriteU32(uint v) + { + Ensure(4); + _buf[_len++] = (byte)v; + _buf[_len++] = (byte)(v >> 8); + _buf[_len++] = (byte)(v >> 16); + _buf[_len++] = (byte)(v >> 24); + } + + internal void WriteI32(int v) + { + WriteU32((uint)v); + } + + internal void WriteU64(ulong v) + { + WriteU32((uint)v); + WriteU32((uint)(v >> 32)); + } + + internal void WriteI64(long v) + { + WriteU64((ulong)v); + } + + internal void WriteF32(float v) + { + WriteU32((uint)BitConverter.SingleToInt32Bits(v)); + } + + internal void WriteF64(double v) + { + WriteU64((ulong)BitConverter.DoubleToInt64Bits(v)); + } + + internal void WriteLen(int len) + { + WriteU32((uint)len); + } + + internal void WriteOptionFlag(bool present) + { + WriteBool(present); + } + + internal void WriteString(string v) + { + var bytes = System.Text.Encoding.UTF8.GetBytes(v); + WriteLen(bytes.Length); + Ensure(bytes.Length); + Array.Copy(bytes, 0, _buf, _len, bytes.Length); + _len += bytes.Length; + } + + internal void WriteBytes(byte[] v) + { + WriteLen(v.Length); + Ensure(v.Length); + Array.Copy(v, 0, _buf, _len, v.Length); + _len += v.Length; + } + + internal byte[] ToArray() + { + var outBuf = new byte[_len]; + Array.Copy(_buf, outBuf, _len); + return outBuf; + } + } + + /// Decodes values from the WeaveFFI value-buffer wire format. + /// A malformed buffer indicates a producer/consumer contract violation and + /// throws . + internal sealed class WeaveFFIBufferReader + { + private static readonly System.Text.Encoding Utf8Strict = + new System.Text.UTF8Encoding(false, true); + + private readonly byte[] _data; + private int _pos; + + internal WeaveFFIBufferReader(byte[] data) + { + _data = data; + } + + private void Require(int n) + { + if (_data.Length - _pos < n) + { + throw new InvalidOperationException("malformed WeaveFFI value buffer: buffer exhausted"); + } + } + + internal bool ReadBool() + { + Require(1); + var b = _data[_pos++]; + if (b > 1) + { + throw new InvalidOperationException("malformed WeaveFFI value buffer: invalid bool byte"); + } + return b == 1; + } + + internal sbyte ReadI8() + { + Require(1); + return (sbyte)_data[_pos++]; + } + + internal byte ReadU8() + { + Require(1); + return _data[_pos++]; + } + + internal ushort ReadU16() + { + Require(2); + var v = (ushort)(_data[_pos] | (_data[_pos + 1] << 8)); + _pos += 2; + return v; + } + + internal short ReadI16() + { + return (short)ReadU16(); + } + + internal uint ReadU32() + { + Require(4); + var v = (uint)_data[_pos] + | ((uint)_data[_pos + 1] << 8) + | ((uint)_data[_pos + 2] << 16) + | ((uint)_data[_pos + 3] << 24); + _pos += 4; + return v; + } + + internal int ReadI32() + { + return (int)ReadU32(); + } + + internal ulong ReadU64() + { + var lo = (ulong)ReadU32(); + var hi = (ulong)ReadU32(); + return lo | (hi << 32); + } + + internal long ReadI64() + { + return (long)ReadU64(); + } + + internal float ReadF32() + { + return BitConverter.Int32BitsToSingle(ReadI32()); + } + + internal double ReadF64() + { + return BitConverter.Int64BitsToDouble(ReadI64()); + } + + internal int ReadLen() + { + var len = ReadU32(); + if (len > (uint)(_data.Length - _pos)) + { + throw new InvalidOperationException("malformed WeaveFFI value buffer: length prefix exceeds remaining bytes"); + } + return (int)len; + } + + internal bool ReadOptionFlag() + { + return ReadBool(); + } + + internal string ReadString() + { + var len = ReadLen(); + string s; + try + { + s = Utf8Strict.GetString(_data, _pos, len); + } + catch (ArgumentException) + { + throw new InvalidOperationException("malformed WeaveFFI value buffer: string is not valid UTF-8"); + } + _pos += len; + return s; + } + + internal byte[] ReadBytes() + { + var len = ReadLen(); + var outBuf = new byte[len]; + Array.Copy(_data, _pos, outBuf, 0, len); + _pos += len; + return outBuf; + } + + internal void ExpectEnd() + { + if (_pos != _data.Length) + { + throw new InvalidOperationException("malformed WeaveFFI value buffer: trailing bytes"); + } + } + } + + /// A typed native handle referencing a Leaf. + public readonly struct LeafHandle + { + internal readonly IntPtr Raw; + + internal LeafHandle(IntPtr raw) + { + Raw = raw; + } + } + + /// A lazily streamed sequence backed by a native iterator. + /// It can be enumerated exactly once; enumerate it promptly (or call + /// a materializing operator such as ToList) and let the enumerator be + /// disposed to release the native iterator. + internal sealed class WeaveFFIOnceEnumerable : IEnumerable + { + private IEnumerator? _enumerator; + + internal WeaveFFIOnceEnumerable(IEnumerator enumerator) + { + _enumerator = enumerator; + } + + public IEnumerator GetEnumerator() + { + var e = System.Threading.Interlocked.Exchange(ref _enumerator, null); + if (e == null) + { + throw new InvalidOperationException("this sequence can be enumerated only once"); + } + return e; + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + /// Deprecated enum with non-contiguous values and keyword-like variants + public enum Legacy + { + None = 0, + Default = 5, + Class = 10, + } + + public enum Mode + { + Fast = 1, + Safe = 2, + } + + public sealed class Leaf + { + public float Weight { get; } + + public Leaf(float weight) + { + Weight = weight; + } + + internal void WriteTo(WeaveFFIBufferWriter writer) + { + writer.WriteF32(Weight); + } + + internal static Leaf ReadFrom(WeaveFFIBufferReader reader) + { + var fWeight = reader.ReadF32(); + return new Leaf(fWeight); + } + } + + /// Middle layer of a three-deep struct nesting + public sealed class Inner + { + public Leaf Leaf { get; } + + public Leaf[] Leaves { get; } + + public Inner(Leaf leaf, Leaf[] leaves) + { + Leaf = leaf; + Leaves = leaves; + } + + internal void WriteTo(WeaveFFIBufferWriter writer) + { + Leaf.WriteTo(writer); + writer.WriteLen(Leaves.Length); + foreach (var item0 in Leaves) + { + item0.WriteTo(writer); + } + } + + internal static Inner ReadFrom(WeaveFFIBufferReader reader) + { + var fLeaf = Leaf.ReadFrom(reader); + var fLeavesCount = reader.ReadLen(); + var fLeaves = new Leaf[fLeavesCount]; + for (int i0 = 0; i0 < fLeavesCount; i0++) + { + var fLeavesItem = Leaf.ReadFrom(reader); + fLeaves[i0] = fLeavesItem; + } + return new Inner(fLeaf, fLeaves); + } + } + + 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; } + + 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(WeaveFFIBufferWriter writer) + { + Inner.WriteTo(writer); + if (MaybeInner != null) + { + writer.WriteOptionFlag(true); + MaybeInner!.WriteTo(writer); + } + else + { + writer.WriteOptionFlag(false); + } + writer.WriteLen(ByName.Count); + foreach (var entry0 in ByName) + { + writer.WriteString(entry0.Key); + entry0.Value.WriteTo(writer); + } + writer.WriteLen(Modes.Length); + foreach (var item0 in Modes) + { + writer.WriteI32((int)item0); + } + writer.WriteLen(Matrix.Length); + foreach (var item0 in Matrix) + { + writer.WriteLen(item0.Length); + foreach (var item1 in item0) + { + writer.WriteI32(item1); + } + } + writer.WriteLen(Sparse.Length); + foreach (var item0 in Sparse) + { + if (item0 != null) + { + writer.WriteOptionFlag(true); + writer.WriteI64(item0.Value); + } + else + { + writer.WriteOptionFlag(false); + } + } + if (Blob != null) + { + writer.WriteOptionFlag(true); + writer.WriteBytes(Blob!); + } + else + { + writer.WriteOptionFlag(false); + } + if (Note != null) + { + writer.WriteOptionFlag(true); + writer.WriteString(Note!); + } + else + { + writer.WriteOptionFlag(false); + } + writer.WriteLen(ById.Count); + foreach (var entry0 in ById) + { + writer.WriteI64(entry0.Key); + writer.WriteString(entry0.Value); + } + writer.WriteBool(Hidden); + writer.WriteU8(V2); + } + + internal static Outer ReadFrom(WeaveFFIBufferReader reader) + { + var fInner = Inner.ReadFrom(reader); + Inner? fMaybeInner = null; + if (reader.ReadOptionFlag()) + { + var fMaybeInnerValue = Inner.ReadFrom(reader); + fMaybeInner = fMaybeInnerValue; + } + var fByNameCount = reader.ReadLen(); + var fByName = new Dictionary(fByNameCount); + for (int i0 = 0; i0 < fByNameCount; i0++) + { + var fByNameKey = reader.ReadString(); + var fByNameVal = Inner.ReadFrom(reader); + fByName[fByNameKey] = fByNameVal; + } + var fModesCount = reader.ReadLen(); + var fModes = new Mode[fModesCount]; + for (int i0 = 0; i0 < fModesCount; i0++) + { + var fModesItem = (Mode)reader.ReadI32(); + fModes[i0] = fModesItem; + } + var fMatrixCount = reader.ReadLen(); + var fMatrix = new int[fMatrixCount][]; + for (int i0 = 0; i0 < fMatrixCount; i0++) + { + var fMatrixItemCount = reader.ReadLen(); + var fMatrixItem = new int[fMatrixItemCount]; + for (int i1 = 0; i1 < fMatrixItemCount; i1++) + { + var fMatrixItemItem = reader.ReadI32(); + fMatrixItem[i1] = fMatrixItemItem; + } + fMatrix[i0] = fMatrixItem; + } + var fSparseCount = reader.ReadLen(); + var fSparse = new long?[fSparseCount]; + for (int i0 = 0; i0 < fSparseCount; i0++) + { + long? fSparseItem = null; + if (reader.ReadOptionFlag()) + { + var fSparseItemValue = reader.ReadI64(); + fSparseItem = fSparseItemValue; + } + fSparse[i0] = fSparseItem; + } + byte[]? fBlob = null; + if (reader.ReadOptionFlag()) + { + var fBlobValue = reader.ReadBytes(); + fBlob = fBlobValue; + } + string? fNote = null; + if (reader.ReadOptionFlag()) + { + var fNoteValue = reader.ReadString(); + fNote = fNoteValue; + } + var fByIdCount = reader.ReadLen(); + var fById = new Dictionary(fByIdCount); + for (int i0 = 0; i0 < fByIdCount; i0++) + { + var fByIdKey = reader.ReadI64(); + var fByIdVal = reader.ReadString(); + fById[fByIdKey] = fByIdVal; + } + var fHidden = reader.ReadBool(); + var fV2 = reader.ReadU8(); + return new Outer(fInner, fMaybeInner, fByName, fModes, fMatrix, fSparse, fBlob, fNote, fById, fHidden, fV2); + } + } + + /// 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; } + + 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(WeaveFFIBufferWriter 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(WeaveFFIBufferReader reader) + { + var fClass = reader.ReadI32(); + var fDef = reader.ReadI32(); + var fImport = reader.ReadI32(); + var fFrom = reader.ReadI32(); + var fFunc = reader.ReadI32(); + var fRange = reader.ReadI32(); + var fRegister = reader.ReadI32(); + var fVolatile = reader.ReadI32(); + var fNew = reader.ReadI32(); + var fDefault = reader.ReadI32(); + var fEnd = reader.ReadI32(); + var fIn = reader.ReadI32(); + var fIs = reader.ReadI32(); + var fInt = reader.ReadI32(); + var fVar = reader.ReadI32(); + var fVal = reader.ReadI32(); + var fSelf = reader.ReadI32(); + var fThis = reader.ReadI32(); + var fObject = reader.ReadI32(); + var fString = reader.ReadI32(); + var fCase = reader.ReadI32(); + var fSelect = reader.ReadI32(); + var fSwitch = reader.ReadI32(); + var fVoid = reader.ReadI32(); + var fUnion = reader.ReadI32(); + var fNamespace = reader.ReadI32(); + var fOperator = reader.ReadI32(); + var fTemplate = reader.ReadI32(); + var fPackage = reader.ReadI32(); + var fProtocol = reader.ReadI32(); + var fGuard = reader.ReadI32(); + var fDefer = reader.ReadI32(); + var fInout = reader.ReadI32(); + var fInternal = reader.ReadI32(); + var fWhen = reader.ReadI32(); + var fFun = reader.ReadI32(); + var fData = reader.ReadI32(); + var fLate = reader.ReadI32(); + var fPart = reader.ReadI32(); + var fOut = reader.ReadI32(); + var fRef = reader.ReadI32(); + var fEvent = reader.ReadI32(); + var fBase = reader.ReadI32(); + var fLambda = reader.ReadI32(); + var fYield = reader.ReadI32(); + var fNil = reader.ReadI32(); + var fNull = reader.ReadI32(); + var fBegin = reader.ReadI32(); + var fRescue = reader.ReadI32(); + var fModule = reader.ReadI32(); + var fChan = reader.ReadI32(); + var fGo = reader.ReadI32(); + var fLet = reader.ReadI32(); + var fExport = reader.ReadI32(); + var fDelete = reader.ReadI32(); + var fTypedef = reader.ReadI32(); + var fSizeof = reader.ReadI32(); + var fStatic = reader.ReadI32(); + var fSuper = reader.ReadI32(); + return new Keywords(fClass, fDef, fImport, fFrom, fFunc, fRange, fRegister, fVolatile, fNew, fDefault, fEnd, fIn, fIs, fInt, fVar, fVal, fSelf, fThis, fObject, fString, fCase, fSelect, fSwitch, fVoid, fUnion, fNamespace, fOperator, fTemplate, fPackage, fProtocol, fGuard, fDefer, fInout, fInternal, fWhen, fFun, fData, fLate, fPart, fOut, fRef, fEvent, fBase, fLambda, fYield, fNil, fNull, fBegin, fRescue, fModule, fChan, fGo, fLet, fExport, fDelete, fTypedef, fSizeof, fStatic, fSuper); + } + } + + /// Deprecated struct + public sealed class Retired + { + public long Id { get; } + + public Retired(long id) + { + Id = id; + } + + internal void WriteTo(WeaveFFIBufferWriter writer) + { + writer.WriteI64(Id); + } + + internal static Retired ReadFrom(WeaveFFIBufferReader reader) + { + var fId = reader.ReadI64(); + return new Retired(fId); + } + } + + /// Interface with no constructor; instances come from make_widget + public class Widget : IDisposable + { + private IntPtr _handle; + private bool _disposed; + + internal Widget(IntPtr handle) + { + _handle = handle; + } + + internal IntPtr Handle => _handle; + + /// Void method that can fail + /// Thrown when the call reports a EdgeErrorsError code. + public void Refresh() + { + var err = new WeaveFFIError(); + NativeMethods.weaveffi_edge_cases_Widget_refresh(_handle, ref err); + WeaveFFIError.CheckEdgeErrors(err); + } + + public string Rename(string @class, string? import) + { + var err = new WeaveFFIError(); + var @classPtr = Marshal.StringToCoTaskMemUTF8(@class); + var importWriter = new WeaveFFIBufferWriter(); + if (import != null) + { + importWriter.WriteOptionFlag(true); + importWriter.WriteString(import!); + } + else + { + importWriter.WriteOptionFlag(false); + } + var importBuf = importWriter.ToArray(); + var importPin = GCHandle.Alloc(importBuf, GCHandleType.Pinned); + try + { + var result = NativeMethods.weaveffi_edge_cases_Widget_rename(_handle, @classPtr, importPin.AddrOfPinnedObject(), (UIntPtr)importBuf.Length, ref err); + WeaveFFIError.Check(err); + var str = Marshal.PtrToStringUTF8(result); + NativeMethods.weaveffi_free_string(result); + return str ?? ""; + } + finally + { + Marshal.FreeCoTaskMem(@classPtr); + importPin.Free(); + } + } + + /// Async method with a struct result + /// Thrown when the call reports a EdgeErrorsError code. + public async Task Fetch(long id) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + NativeMethods.AsyncCb_weaveffi_edge_cases_Widget_fetch callback = (context, err, result, resultLen) => + { + try + { + if (err != IntPtr.Zero) + { + var wErr = Marshal.PtrToStructure(err); + if (wErr.Code != 0) + { + var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? ""; + var payload = WeaveFFIError.CopyPayload(wErr); + NativeMethods.weaveffi_error_free(err); + tcs.SetException(EdgeErrorsException.FromCode(wErr.Code, msg, payload)); + return; + } + } + var resultBuf = new byte[(int)resultLen]; + if (result != IntPtr.Zero && (int)resultLen > 0) Marshal.Copy(result, resultBuf, 0, (int)resultLen); + if (result != IntPtr.Zero) NativeMethods.weaveffi_free_bytes(result, resultLen); + var decodedReader = new WeaveFFIBufferReader(resultBuf); + var decoded = Outer.ReadFrom(decodedReader); + decodedReader.ExpectEnd(); + tcs.SetResult(decoded); + } + finally + { + if (context != IntPtr.Zero) + { + GCHandle.FromIntPtr(context).Free(); + } + } + }; + var gcHandle = GCHandle.Alloc(callback, GCHandleType.Normal); + var ctx = GCHandle.ToIntPtr(gcHandle); + try + { + NativeMethods.weaveffi_edge_cases_Widget_fetch_async(_handle, id, callback, ctx); + } + catch + { + if (gcHandle.IsAllocated) gcHandle.Free(); + throw; + } + return await tcs.Task; + } + + [Obsolete("Use rename instead")] + public string OldName() + { + var err = new WeaveFFIError(); + var result = NativeMethods.weaveffi_edge_cases_Widget_old_name(_handle, ref err); + WeaveFFIError.Check(err); + var str = Marshal.PtrToStringUTF8(result); + NativeMethods.weaveffi_free_string(result); + return str ?? ""; + } + + public void Dispose() + { + if (!_disposed) + { + NativeMethods.weaveffi_edge_cases_Widget_destroy(_handle); + _disposed = true; + } + GC.SuppressFinalize(this); + } + + ~Widget() + { + Dispose(); + } + } + + /// Deprecated interface with a throwing constructor + public class Vault : IDisposable + { + private IntPtr _handle; + private bool _disposed; + + internal Vault(IntPtr handle) + { + _handle = handle; + } + + internal IntPtr Handle => _handle; + + /// Fails when the vault is busy + /// Thrown when the call reports a EdgeErrorsError code. + public static Vault Open(string path) + { + var err = new WeaveFFIError(); + var pathPtr = Marshal.StringToCoTaskMemUTF8(path); + try + { + var result = NativeMethods.weaveffi_edge_cases_Vault_open(pathPtr, ref err); + WeaveFFIError.CheckEdgeErrors(err); + return new Vault(result); + } + finally + { + Marshal.FreeCoTaskMem(pathPtr); + } + } + + public Widget? Peek() + { + var err = new WeaveFFIError(); + var result = NativeMethods.weaveffi_edge_cases_Vault_peek(_handle, ref err); + WeaveFFIError.Check(err); + return result == IntPtr.Zero ? null : new Widget(result); + } + + /// Throwing static + /// Thrown when the call reports a EdgeErrorsError code. + public static bool Probe() + { + var err = new WeaveFFIError(); + var result = NativeMethods.weaveffi_edge_cases_Vault_probe(ref err); + WeaveFFIError.CheckEdgeErrors(err); + return result != 0; + } + + public void Dispose() + { + if (!_disposed) + { + NativeMethods.weaveffi_edge_cases_Vault_destroy(_handle); + _disposed = true; + } + GC.SuppressFinalize(this); + } + + ~Vault() + { + Dispose(); + } + } + + internal static class NativeMethods + { + private const string LibName = "weaveffi"; + + // The ABI revision these bindings were generated against. + internal const uint AbiVersion = 1; + + // Runs before the first P/Invoke through this class, so a producer built + // for a different ABI revision fails loudly instead of misreading the + // error struct or a value buffer later. + static NativeMethods() + { + uint found; + try + { + found = weaveffi_abi_version(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded WeaveFFI library predates ABI versioning (these bindings expect ABI revision {AbiVersion})", e); + } + if (found != AbiVersion) + { + throw new InvalidOperationException( + $"WeaveFFI ABI mismatch: these bindings expect revision {AbiVersion} but the loaded library reports revision {found}"); + } + } + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern uint weaveffi_abi_version(); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_free_string(IntPtr ptr); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_free_bytes(IntPtr ptr, UIntPtr len); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_error_clear(ref WeaveFFIError err); + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_error_free(IntPtr err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_Widget_destroy", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_Widget_destroy(IntPtr self); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_Widget_refresh", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_Widget_refresh(IntPtr self, ref WeaveFFIError err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_Widget_rename", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr weaveffi_edge_cases_Widget_rename(IntPtr self, IntPtr @class, IntPtr import_ptr, UIntPtr import_len, ref WeaveFFIError err); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void AsyncCb_weaveffi_edge_cases_Widget_fetch(IntPtr context, IntPtr err, IntPtr result, UIntPtr resultLen); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_Widget_fetch_async", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_Widget_fetch_async(IntPtr self, long id, AsyncCb_weaveffi_edge_cases_Widget_fetch callback, IntPtr context); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_Widget_old_name", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr weaveffi_edge_cases_Widget_old_name(IntPtr self, ref WeaveFFIError err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_Vault_destroy", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_Vault_destroy(IntPtr self); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_Vault_open", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr weaveffi_edge_cases_Vault_open(IntPtr path, ref WeaveFFIError err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_Vault_peek", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr weaveffi_edge_cases_Vault_peek(IntPtr self, ref WeaveFFIError err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_Vault_probe", CallingConvention = CallingConvention.Cdecl)] + internal static extern byte weaveffi_edge_cases_Vault_probe(ref WeaveFFIError err); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void Cb_weaveffi_edge_cases_OnBatch_fn(IntPtr items_ptr, UIntPtr items_len, IntPtr raw_ptr, UIntPtr raw_len, IntPtr first_ptr, UIntPtr first_len, int mode, IntPtr context); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void Cb_weaveffi_edge_cases_OnTick_fn(IntPtr context); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_register_batch_listener", CallingConvention = CallingConvention.Cdecl)] + internal static extern ulong weaveffi_edge_cases_register_batch_listener(Cb_weaveffi_edge_cases_OnBatch_fn callback, IntPtr context); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_unregister_batch_listener", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_unregister_batch_listener(ulong id); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_register_tick_listener", CallingConvention = CallingConvention.Cdecl)] + internal static extern ulong weaveffi_edge_cases_register_tick_listener(Cb_weaveffi_edge_cases_OnTick_fn callback, IntPtr context); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_unregister_tick_listener", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_unregister_tick_listener(ulong id); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_noop", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_noop(ref WeaveFFIError err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_select", CallingConvention = CallingConvention.Cdecl)] + internal static extern int weaveffi_edge_cases_select(int @class, IntPtr from, IntPtr func_ptr, UIntPtr func_len, IntPtr range_ptr, UIntPtr range_len, byte type_, ref WeaveFFIError err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_nested_optional", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr weaveffi_edge_cases_nested_optional(IntPtr items_ptr, UIntPtr items_len, out UIntPtr out_len, ref WeaveFFIError err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_optional_params", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr weaveffi_edge_cases_optional_params(IntPtr text_ptr, UIntPtr text_len, IntPtr count_ptr, UIntPtr count_len, IntPtr outer_ptr, UIntPtr outer_len, IntPtr tags_ptr, UIntPtr tags_len, IntPtr blob_ptr, UIntPtr blob_len, IntPtr mode_ptr, UIntPtr mode_len, out UIntPtr out_len, ref WeaveFFIError err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_mutable_params", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_mutable_params(IntPtr text, IntPtr blob_ptr, UIntPtr blob_len, ref WeaveFFIError err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_make_widget", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr weaveffi_edge_cases_make_widget(IntPtr label, ref WeaveFFIError err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_find_widget", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr weaveffi_edge_cases_find_widget(long id, ref WeaveFFIError err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_inspect_widget", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr weaveffi_edge_cases_inspect_widget(IntPtr widget, IntPtr fallback, ref WeaveFFIError err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_typed_handle_param", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr weaveffi_edge_cases_typed_handle_param(IntPtr token, ulong raw, ref WeaveFFIError err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_string_stream", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr weaveffi_edge_cases_string_stream(ref WeaveFFIError out_err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_StringStreamIterator_next", CallingConvention = CallingConvention.Cdecl)] + internal static extern int weaveffi_edge_cases_StringStreamIterator_next(IntPtr iter, out IntPtr out_item, ref WeaveFFIError out_err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_StringStreamIterator_destroy", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_StringStreamIterator_destroy(IntPtr iter); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_scalar_stream", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr weaveffi_edge_cases_scalar_stream(ref WeaveFFIError out_err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_ScalarStreamIterator_next", CallingConvention = CallingConvention.Cdecl)] + internal static extern int weaveffi_edge_cases_ScalarStreamIterator_next(IntPtr iter, out int out_item, ref WeaveFFIError out_err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_ScalarStreamIterator_destroy", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_ScalarStreamIterator_destroy(IntPtr iter); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_mode_stream", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr weaveffi_edge_cases_mode_stream(ref WeaveFFIError out_err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_ModeStreamIterator_next", CallingConvention = CallingConvention.Cdecl)] + internal static extern int weaveffi_edge_cases_ModeStreamIterator_next(IntPtr iter, out int out_item, ref WeaveFFIError out_err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_ModeStreamIterator_destroy", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_ModeStreamIterator_destroy(IntPtr iter); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void AsyncCb_weaveffi_edge_cases_async_void(IntPtr context, IntPtr err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_async_void_async", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_async_void_async(AsyncCb_weaveffi_edge_cases_async_void callback, IntPtr context); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void AsyncCb_weaveffi_edge_cases_async_bytes(IntPtr context, IntPtr err, IntPtr result, UIntPtr resultLen); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_async_bytes_async", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_async_bytes_async(uint size, AsyncCb_weaveffi_edge_cases_async_bytes callback, IntPtr context); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void AsyncCb_weaveffi_edge_cases_async_struct(IntPtr context, IntPtr err, IntPtr result, UIntPtr resultLen); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_async_struct_async", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_async_struct_async(AsyncCb_weaveffi_edge_cases_async_struct callback, IntPtr context); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void AsyncCb_weaveffi_edge_cases_async_list(IntPtr context, IntPtr err, IntPtr result, UIntPtr resultLen); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_async_list_async", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_async_list_async(IntPtr cancel_token, AsyncCb_weaveffi_edge_cases_async_list callback, IntPtr context); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void AsyncCb_weaveffi_edge_cases_async_optional(IntPtr context, IntPtr err, IntPtr result, UIntPtr resultLen); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_async_optional_async", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_async_optional_async(AsyncCb_weaveffi_edge_cases_async_optional callback, IntPtr context); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void AsyncCb_weaveffi_edge_cases_async_enum(IntPtr context, IntPtr err, int result); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_async_enum_async", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_async_enum_async(AsyncCb_weaveffi_edge_cases_async_enum callback, IntPtr context); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void AsyncCb_weaveffi_edge_cases_async_keywords(IntPtr context, IntPtr err, IntPtr result); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_async_keywords_async", CallingConvention = CallingConvention.Cdecl)] + internal static extern void weaveffi_edge_cases_async_keywords_async(IntPtr @class, int import, AsyncCb_weaveffi_edge_cases_async_keywords callback, IntPtr context); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_keywords_roundtrip", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr weaveffi_edge_cases_keywords_roundtrip(IntPtr value_ptr, UIntPtr value_len, out UIntPtr out_len, ref WeaveFFIError err); + + [DllImport(LibName, EntryPoint = "weaveffi_edge_cases_legacy_value", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr weaveffi_edge_cases_legacy_value(int value, out UIntPtr out_len, ref WeaveFFIError err); + + } + + public static class EdgeCases + { + private static readonly object _listenerLock = new object(); + // Live listener delegates by subscription id. Holding the delegate + // here keeps its native thunk alive until unregistered; without this + // the GC could collect a delegate the producer still calls. + private static readonly Dictionary _listenerRefs = new Dictionary(); + + /// A subscription id for UnregisterBatchListener(). + public static ulong RegisterBatchListener(Action callback) + { + NativeMethods.Cb_weaveffi_edge_cases_OnBatch_fn trampoline = (items_ptr, items_len, raw_ptr, raw_len, first_ptr, first_len, mode, context) => + { + var arg0Buf = new byte[(int)items_len]; + if (items_ptr != IntPtr.Zero && (int)items_len > 0) Marshal.Copy(items_ptr, arg0Buf, 0, (int)items_len); + var arg0Reader = new WeaveFFIBufferReader(arg0Buf); + var arg0Count = arg0Reader.ReadLen(); + var arg0 = new Outer[arg0Count]; + for (int i0 = 0; i0 < arg0Count; i0++) + { + var arg0Item = Outer.ReadFrom(arg0Reader); + arg0[i0] = arg0Item; + } + arg0Reader.ExpectEnd(); + var arg1 = new byte[(int)raw_len]; + if (raw_ptr != IntPtr.Zero && (int)raw_len > 0) Marshal.Copy(raw_ptr, arg1, 0, (int)raw_len); + var arg2Buf = new byte[(int)first_len]; + if (first_ptr != IntPtr.Zero && (int)first_len > 0) Marshal.Copy(first_ptr, arg2Buf, 0, (int)first_len); + var arg2Reader = new WeaveFFIBufferReader(arg2Buf); + Outer? arg2 = null; + if (arg2Reader.ReadOptionFlag()) + { + var arg2Value = Outer.ReadFrom(arg2Reader); + arg2 = arg2Value; + } + arg2Reader.ExpectEnd(); + callback(arg0, arg1, arg2, (Mode)mode); + }; + ulong id; + lock (_listenerLock) + { + id = NativeMethods.weaveffi_edge_cases_register_batch_listener(trampoline, IntPtr.Zero); + _listenerRefs[id] = trampoline; + } + return id; + } + + /// Unregisters a listener previously registered with RegisterBatchListener(). + public static void UnregisterBatchListener(ulong id) + { + NativeMethods.weaveffi_edge_cases_unregister_batch_listener(id); + lock (_listenerLock) + { + _listenerRefs.Remove(id); + } + } + + /// A subscription id for UnregisterTickListener(). + public static ulong RegisterTickListener(Action callback) + { + NativeMethods.Cb_weaveffi_edge_cases_OnTick_fn trampoline = (context) => + { + callback(); + }; + ulong id; + lock (_listenerLock) + { + id = NativeMethods.weaveffi_edge_cases_register_tick_listener(trampoline, IntPtr.Zero); + _listenerRefs[id] = trampoline; + } + return id; + } + + /// Unregisters a listener previously registered with RegisterTickListener(). + public static void UnregisterTickListener(ulong id) + { + NativeMethods.weaveffi_edge_cases_unregister_tick_listener(id); + lock (_listenerLock) + { + _listenerRefs.Remove(id); + } + } + + /// No parameters and no result + public static void Noop() + { + var err = new WeaveFFIError(); + NativeMethods.weaveffi_edge_cases_noop(ref err); + WeaveFFIError.Check(err); + } + + /// 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 err = new WeaveFFIError(); + var fromPtr = Marshal.StringToCoTaskMemUTF8(from); + var funcWriter = new WeaveFFIBufferWriter(); + funcWriter.WriteLen(func.Length); + foreach (var item0 in func) + { + funcWriter.WriteString(item0); + } + var funcBuf = funcWriter.ToArray(); + var funcPin = GCHandle.Alloc(funcBuf, GCHandleType.Pinned); + var rangeWriter = new WeaveFFIBufferWriter(); + if (range != null) + { + rangeWriter.WriteOptionFlag(true); + rangeWriter.WriteI32(range.Value); + } + else + { + rangeWriter.WriteOptionFlag(false); + } + var rangeBuf = rangeWriter.ToArray(); + var rangePin = GCHandle.Alloc(rangeBuf, GCHandleType.Pinned); + try + { + var result = NativeMethods.weaveffi_edge_cases_select(@class, fromPtr, funcPin.AddrOfPinnedObject(), (UIntPtr)funcBuf.Length, rangePin.AddrOfPinnedObject(), (UIntPtr)rangeBuf.Length, (byte)(type ? 1 : 0), ref err); + WeaveFFIError.Check(err); + return result; + } + finally + { + Marshal.FreeCoTaskMem(fromPtr); + funcPin.Free(); + rangePin.Free(); + } + } + + /// Optional list of optional structs, the deepest legal nesting + public static Outer?[]? NestedOptional(Outer?[]? items) + { + var err = new WeaveFFIError(); + var itemsWriter = new WeaveFFIBufferWriter(); + if (items != null) + { + itemsWriter.WriteOptionFlag(true); + itemsWriter.WriteLen(items!.Length); + foreach (var item0 in items!) + { + if (item0 != null) + { + itemsWriter.WriteOptionFlag(true); + item0!.WriteTo(itemsWriter); + } + else + { + itemsWriter.WriteOptionFlag(false); + } + } + } + else + { + itemsWriter.WriteOptionFlag(false); + } + var itemsBuf = itemsWriter.ToArray(); + var itemsPin = GCHandle.Alloc(itemsBuf, GCHandleType.Pinned); + try + { + var result = NativeMethods.weaveffi_edge_cases_nested_optional(itemsPin.AddrOfPinnedObject(), (UIntPtr)itemsBuf.Length, out var outLen, ref err); + WeaveFFIError.Check(err); + var resultBuf = new byte[(int)outLen]; + if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); + NativeMethods.weaveffi_free_bytes(result, outLen); + var decodedReader = new WeaveFFIBufferReader(resultBuf); + Outer?[]? decoded = null; + if (decodedReader.ReadOptionFlag()) + { + var decodedValueCount = decodedReader.ReadLen(); + var decodedValue = new Outer?[decodedValueCount]; + for (int i0 = 0; i0 < decodedValueCount; i0++) + { + Outer? decodedValueItem = null; + if (decodedReader.ReadOptionFlag()) + { + var decodedValueItemValue = Outer.ReadFrom(decodedReader); + decodedValueItem = decodedValueItemValue; + } + decodedValue[i0] = decodedValueItem; + } + decoded = decodedValue; + } + decodedReader.ExpectEnd(); + return decoded; + } + finally + { + itemsPin.Free(); + } + } + + public static Mode? OptionalParams(string? text, int? count, Outer? outer, string[]? tags, byte[]? blob, Mode? mode) + { + var err = new WeaveFFIError(); + var textWriter = new WeaveFFIBufferWriter(); + if (text != null) + { + textWriter.WriteOptionFlag(true); + textWriter.WriteString(text!); + } + else + { + textWriter.WriteOptionFlag(false); + } + var textBuf = textWriter.ToArray(); + var textPin = GCHandle.Alloc(textBuf, GCHandleType.Pinned); + var countWriter = new WeaveFFIBufferWriter(); + if (count != null) + { + countWriter.WriteOptionFlag(true); + countWriter.WriteI32(count.Value); + } + else + { + countWriter.WriteOptionFlag(false); + } + var countBuf = countWriter.ToArray(); + var countPin = GCHandle.Alloc(countBuf, GCHandleType.Pinned); + var outerWriter = new WeaveFFIBufferWriter(); + if (outer != null) + { + outerWriter.WriteOptionFlag(true); + outer!.WriteTo(outerWriter); + } + else + { + outerWriter.WriteOptionFlag(false); + } + var outerBuf = outerWriter.ToArray(); + var outerPin = GCHandle.Alloc(outerBuf, GCHandleType.Pinned); + var tagsWriter = new WeaveFFIBufferWriter(); + if (tags != null) + { + tagsWriter.WriteOptionFlag(true); + tagsWriter.WriteLen(tags!.Length); + foreach (var item0 in tags!) + { + tagsWriter.WriteString(item0); + } + } + else + { + tagsWriter.WriteOptionFlag(false); + } + var tagsBuf = tagsWriter.ToArray(); + var tagsPin = GCHandle.Alloc(tagsBuf, GCHandleType.Pinned); + var blobWriter = new WeaveFFIBufferWriter(); + if (blob != null) + { + blobWriter.WriteOptionFlag(true); + blobWriter.WriteBytes(blob!); + } + else + { + blobWriter.WriteOptionFlag(false); + } + var blobBuf = blobWriter.ToArray(); + var blobPin = GCHandle.Alloc(blobBuf, GCHandleType.Pinned); + var modeWriter = new WeaveFFIBufferWriter(); + if (mode != null) + { + modeWriter.WriteOptionFlag(true); + modeWriter.WriteI32((int)mode.Value); + } + else + { + modeWriter.WriteOptionFlag(false); + } + var modeBuf = modeWriter.ToArray(); + var modePin = GCHandle.Alloc(modeBuf, GCHandleType.Pinned); + try + { + var result = NativeMethods.weaveffi_edge_cases_optional_params(textPin.AddrOfPinnedObject(), (UIntPtr)textBuf.Length, countPin.AddrOfPinnedObject(), (UIntPtr)countBuf.Length, outerPin.AddrOfPinnedObject(), (UIntPtr)outerBuf.Length, tagsPin.AddrOfPinnedObject(), (UIntPtr)tagsBuf.Length, blobPin.AddrOfPinnedObject(), (UIntPtr)blobBuf.Length, modePin.AddrOfPinnedObject(), (UIntPtr)modeBuf.Length, out var outLen, ref err); + WeaveFFIError.Check(err); + var resultBuf = new byte[(int)outLen]; + if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); + NativeMethods.weaveffi_free_bytes(result, outLen); + var decodedReader = new WeaveFFIBufferReader(resultBuf); + Mode? decoded = null; + if (decodedReader.ReadOptionFlag()) + { + var decodedValue = (Mode)decodedReader.ReadI32(); + decoded = decodedValue; + } + decodedReader.ExpectEnd(); + return decoded; + } + finally + { + textPin.Free(); + countPin.Free(); + outerPin.Free(); + tagsPin.Free(); + blobPin.Free(); + modePin.Free(); + } + } + + /// Write-back parameters + public static void MutableParams(string text, byte[] blob) + { + var err = new WeaveFFIError(); + var textPtr = Marshal.StringToCoTaskMemUTF8(text); + var blobPin = GCHandle.Alloc(blob, GCHandleType.Pinned); + try + { + NativeMethods.weaveffi_edge_cases_mutable_params(textPtr, blobPin.AddrOfPinnedObject(), (UIntPtr)blob.Length, ref err); + WeaveFFIError.Check(err); + } + finally + { + Marshal.FreeCoTaskMem(textPtr); + blobPin.Free(); + } + } + + /// Factory for an interface with no constructor + public static Widget MakeWidget(string label) + { + var err = new WeaveFFIError(); + var labelPtr = Marshal.StringToCoTaskMemUTF8(label); + try + { + var result = NativeMethods.weaveffi_edge_cases_make_widget(labelPtr, ref err); + WeaveFFIError.Check(err); + return new Widget(result); + } + finally + { + Marshal.FreeCoTaskMem(labelPtr); + } + } + + /// Optional interface return + /// Thrown when the call reports a EdgeErrorsError code. + public static Widget? FindWidget(long id) + { + var err = new WeaveFFIError(); + var result = NativeMethods.weaveffi_edge_cases_find_widget(id, ref err); + WeaveFFIError.CheckEdgeErrors(err); + return result == IntPtr.Zero ? null : new Widget(result); + } + + /// Interface parameter, including an optional one + public static string InspectWidget(Widget widget, Widget? fallback) + { + var err = new WeaveFFIError(); + var result = NativeMethods.weaveffi_edge_cases_inspect_widget(widget.Handle, fallback?.Handle ?? IntPtr.Zero, ref err); + WeaveFFIError.Check(err); + var str = Marshal.PtrToStringUTF8(result); + NativeMethods.weaveffi_free_string(result); + return str ?? ""; + } + + public static LeafHandle TypedHandleParam(LeafHandle token, ulong raw) + { + var err = new WeaveFFIError(); + var result = NativeMethods.weaveffi_edge_cases_typed_handle_param(token.Raw, raw, ref err); + WeaveFFIError.Check(err); + return new LeafHandle(result); + } + + /// Iterator of strings + /// Streams lazily: each element is pulled from the native + /// iterator on demand, and the iterator is destroyed when enumeration + /// completes or the enumerator is disposed (a foreach disposes it + /// automatically, including on early exit). The returned sequence can be + /// enumerated only once. + public static IEnumerable StringStream() + { + var err = new WeaveFFIError(); + var iter = NativeMethods.weaveffi_edge_cases_string_stream(ref err); + WeaveFFIError.Check(err); + return new WeaveFFIOnceEnumerable(EnumerateStringStream(iter)); + } + + private static IEnumerator EnumerateStringStream(IntPtr iter) + { + try + { + while (true) + { + var iterErr = new WeaveFFIError(); + if (NativeMethods.weaveffi_edge_cases_StringStreamIterator_next(iter, out var out_item, ref iterErr) == 0) + { + WeaveFFIError.Check(iterErr); + yield break; + } + WeaveFFIError.Check(iterErr); + var item = Marshal.PtrToStringUTF8(out_item) ?? ""; + NativeMethods.weaveffi_free_string(out_item); + yield return item; + } + } + finally + { + NativeMethods.weaveffi_edge_cases_StringStreamIterator_destroy(iter); + } + } + + /// Iterator of scalars + /// Streams lazily: each element is pulled from the native + /// iterator on demand, and the iterator is destroyed when enumeration + /// completes or the enumerator is disposed (a foreach disposes it + /// automatically, including on early exit). The returned sequence can be + /// enumerated only once. + public static IEnumerable ScalarStream() + { + var err = new WeaveFFIError(); + var iter = NativeMethods.weaveffi_edge_cases_scalar_stream(ref err); + WeaveFFIError.Check(err); + return new WeaveFFIOnceEnumerable(EnumerateScalarStream(iter)); + } + + private static IEnumerator EnumerateScalarStream(IntPtr iter) + { + try + { + while (true) + { + var iterErr = new WeaveFFIError(); + if (NativeMethods.weaveffi_edge_cases_ScalarStreamIterator_next(iter, out var out_item, ref iterErr) == 0) + { + WeaveFFIError.Check(iterErr); + yield break; + } + WeaveFFIError.Check(iterErr); + yield return out_item; + } + } + finally + { + NativeMethods.weaveffi_edge_cases_ScalarStreamIterator_destroy(iter); + } + } + + /// Iterator of enums + /// Streams lazily: each element is pulled from the native + /// iterator on demand, and the iterator is destroyed when enumeration + /// completes or the enumerator is disposed (a foreach disposes it + /// automatically, including on early exit). The returned sequence can be + /// enumerated only once. + public static IEnumerable ModeStream() + { + var err = new WeaveFFIError(); + var iter = NativeMethods.weaveffi_edge_cases_mode_stream(ref err); + WeaveFFIError.Check(err); + return new WeaveFFIOnceEnumerable(EnumerateModeStream(iter)); + } + + private static IEnumerator EnumerateModeStream(IntPtr iter) + { + try + { + while (true) + { + var iterErr = new WeaveFFIError(); + if (NativeMethods.weaveffi_edge_cases_ModeStreamIterator_next(iter, out var out_item, ref iterErr) == 0) + { + WeaveFFIError.Check(iterErr); + yield break; + } + WeaveFFIError.Check(iterErr); + yield return (Mode)out_item; + } + } + finally + { + NativeMethods.weaveffi_edge_cases_ModeStreamIterator_destroy(iter); + } + } + + /// Async with no result + public static async Task AsyncVoid() + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + NativeMethods.AsyncCb_weaveffi_edge_cases_async_void callback = (context, err) => + { + try + { + if (err != IntPtr.Zero) + { + var wErr = Marshal.PtrToStructure(err); + if (wErr.Code != 0) + { + var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? ""; + NativeMethods.weaveffi_error_free(err); + tcs.SetException(new WeaveFFIException(wErr.Code, msg)); + return; + } + } + tcs.SetResult(true); + } + finally + { + if (context != IntPtr.Zero) + { + GCHandle.FromIntPtr(context).Free(); + } + } + }; + var gcHandle = GCHandle.Alloc(callback, GCHandleType.Normal); + var ctx = GCHandle.ToIntPtr(gcHandle); + try + { + NativeMethods.weaveffi_edge_cases_async_void_async(callback, ctx); + } + catch + { + if (gcHandle.IsAllocated) gcHandle.Free(); + throw; + } + await tcs.Task; + } + + public static async Task AsyncBytes(uint size) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + NativeMethods.AsyncCb_weaveffi_edge_cases_async_bytes callback = (context, err, result, resultLen) => + { + try + { + if (err != IntPtr.Zero) + { + var wErr = Marshal.PtrToStructure(err); + if (wErr.Code != 0) + { + var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? ""; + NativeMethods.weaveffi_error_free(err); + tcs.SetException(new WeaveFFIException(wErr.Code, msg)); + return; + } + } + var arr = new byte[(int)resultLen]; + if (result != IntPtr.Zero && (int)resultLen > 0) Marshal.Copy(result, arr, 0, (int)resultLen); + if (result != IntPtr.Zero) NativeMethods.weaveffi_free_bytes(result, resultLen); + tcs.SetResult(arr); + } + finally + { + if (context != IntPtr.Zero) + { + GCHandle.FromIntPtr(context).Free(); + } + } + }; + var gcHandle = GCHandle.Alloc(callback, GCHandleType.Normal); + var ctx = GCHandle.ToIntPtr(gcHandle); + try + { + NativeMethods.weaveffi_edge_cases_async_bytes_async(size, callback, ctx); + } + catch + { + if (gcHandle.IsAllocated) gcHandle.Free(); + throw; + } + return await tcs.Task; + } + + /// Thrown when the call reports a EdgeErrorsError code. + public static async Task AsyncStruct() + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + NativeMethods.AsyncCb_weaveffi_edge_cases_async_struct callback = (context, err, result, resultLen) => + { + try + { + if (err != IntPtr.Zero) + { + var wErr = Marshal.PtrToStructure(err); + if (wErr.Code != 0) + { + var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? ""; + var payload = WeaveFFIError.CopyPayload(wErr); + NativeMethods.weaveffi_error_free(err); + tcs.SetException(EdgeErrorsException.FromCode(wErr.Code, msg, payload)); + return; + } + } + var resultBuf = new byte[(int)resultLen]; + if (result != IntPtr.Zero && (int)resultLen > 0) Marshal.Copy(result, resultBuf, 0, (int)resultLen); + if (result != IntPtr.Zero) NativeMethods.weaveffi_free_bytes(result, resultLen); + var decodedReader = new WeaveFFIBufferReader(resultBuf); + var decoded = Outer.ReadFrom(decodedReader); + decodedReader.ExpectEnd(); + tcs.SetResult(decoded); + } + finally + { + if (context != IntPtr.Zero) + { + GCHandle.FromIntPtr(context).Free(); + } + } + }; + var gcHandle = GCHandle.Alloc(callback, GCHandleType.Normal); + var ctx = GCHandle.ToIntPtr(gcHandle); + try + { + NativeMethods.weaveffi_edge_cases_async_struct_async(callback, ctx); + } + catch + { + if (gcHandle.IsAllocated) gcHandle.Free(); + throw; + } + return await tcs.Task; + } + + /// Thrown when the call reports a EdgeErrorsError code. + public static async Task AsyncList() + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + NativeMethods.AsyncCb_weaveffi_edge_cases_async_list callback = (context, err, result, resultLen) => + { + try + { + if (err != IntPtr.Zero) + { + var wErr = Marshal.PtrToStructure(err); + if (wErr.Code != 0) + { + var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? ""; + var payload = WeaveFFIError.CopyPayload(wErr); + NativeMethods.weaveffi_error_free(err); + tcs.SetException(EdgeErrorsException.FromCode(wErr.Code, msg, payload)); + return; + } + } + var resultBuf = new byte[(int)resultLen]; + if (result != IntPtr.Zero && (int)resultLen > 0) Marshal.Copy(result, resultBuf, 0, (int)resultLen); + if (result != IntPtr.Zero) NativeMethods.weaveffi_free_bytes(result, resultLen); + var decodedReader = new WeaveFFIBufferReader(resultBuf); + var decodedCount = decodedReader.ReadLen(); + var decoded = new Outer[decodedCount]; + for (int i0 = 0; i0 < decodedCount; i0++) + { + var decodedItem = Outer.ReadFrom(decodedReader); + decoded[i0] = decodedItem; + } + decodedReader.ExpectEnd(); + tcs.SetResult(decoded); + } + finally + { + if (context != IntPtr.Zero) + { + GCHandle.FromIntPtr(context).Free(); + } + } + }; + var gcHandle = GCHandle.Alloc(callback, GCHandleType.Normal); + var ctx = GCHandle.ToIntPtr(gcHandle); + try + { + NativeMethods.weaveffi_edge_cases_async_list_async(IntPtr.Zero, callback, ctx); + } + catch + { + if (gcHandle.IsAllocated) gcHandle.Free(); + throw; + } + return await tcs.Task; + } + + public static async Task AsyncOptional() + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + NativeMethods.AsyncCb_weaveffi_edge_cases_async_optional callback = (context, err, result, resultLen) => + { + try + { + if (err != IntPtr.Zero) + { + var wErr = Marshal.PtrToStructure(err); + if (wErr.Code != 0) + { + var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? ""; + NativeMethods.weaveffi_error_free(err); + tcs.SetException(new WeaveFFIException(wErr.Code, msg)); + return; + } + } + var resultBuf = new byte[(int)resultLen]; + if (result != IntPtr.Zero && (int)resultLen > 0) Marshal.Copy(result, resultBuf, 0, (int)resultLen); + if (result != IntPtr.Zero) NativeMethods.weaveffi_free_bytes(result, resultLen); + var decodedReader = new WeaveFFIBufferReader(resultBuf); + long? decoded = null; + if (decodedReader.ReadOptionFlag()) + { + var decodedValue = decodedReader.ReadI64(); + decoded = decodedValue; + } + decodedReader.ExpectEnd(); + tcs.SetResult(decoded); + } + finally + { + if (context != IntPtr.Zero) + { + GCHandle.FromIntPtr(context).Free(); + } + } + }; + var gcHandle = GCHandle.Alloc(callback, GCHandleType.Normal); + var ctx = GCHandle.ToIntPtr(gcHandle); + try + { + NativeMethods.weaveffi_edge_cases_async_optional_async(callback, ctx); + } + catch + { + if (gcHandle.IsAllocated) gcHandle.Free(); + throw; + } + return await tcs.Task; + } + + public static async Task AsyncEnum() + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + NativeMethods.AsyncCb_weaveffi_edge_cases_async_enum callback = (context, err, result) => + { + try + { + if (err != IntPtr.Zero) + { + var wErr = Marshal.PtrToStructure(err); + if (wErr.Code != 0) + { + var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? ""; + NativeMethods.weaveffi_error_free(err); + tcs.SetException(new WeaveFFIException(wErr.Code, msg)); + return; + } + } + tcs.SetResult((Mode)result); + } + finally + { + if (context != IntPtr.Zero) + { + GCHandle.FromIntPtr(context).Free(); + } + } + }; + var gcHandle = GCHandle.Alloc(callback, GCHandleType.Normal); + var ctx = GCHandle.ToIntPtr(gcHandle); + try + { + NativeMethods.weaveffi_edge_cases_async_enum_async(callback, ctx); + } + catch + { + if (gcHandle.IsAllocated) gcHandle.Free(); + throw; + } + return await tcs.Task; + } + + public static async Task AsyncKeywords(string @class, int import) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + NativeMethods.AsyncCb_weaveffi_edge_cases_async_keywords callback = (context, err, result) => + { + try + { + if (err != IntPtr.Zero) + { + var wErr = Marshal.PtrToStructure(err); + if (wErr.Code != 0) + { + var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? ""; + NativeMethods.weaveffi_error_free(err); + tcs.SetException(new WeaveFFIException(wErr.Code, msg)); + return; + } + } + var str = Marshal.PtrToStringUTF8(result) ?? ""; + if (result != IntPtr.Zero) NativeMethods.weaveffi_free_string(result); + tcs.SetResult(str); + } + finally + { + if (context != IntPtr.Zero) + { + GCHandle.FromIntPtr(context).Free(); + } + } + }; + var gcHandle = GCHandle.Alloc(callback, GCHandleType.Normal); + var ctx = GCHandle.ToIntPtr(gcHandle); + var @classPtr = Marshal.StringToCoTaskMemUTF8(@class); + try + { + try + { + NativeMethods.weaveffi_edge_cases_async_keywords_async(@classPtr, import, callback, ctx); + } + catch + { + if (gcHandle.IsAllocated) gcHandle.Free(); + throw; + } + } + finally + { + Marshal.FreeCoTaskMem(@classPtr); + } + return await tcs.Task; + } + + public static Keywords KeywordsRoundtrip(Keywords value) + { + var err = new WeaveFFIError(); + var valueWriter = new WeaveFFIBufferWriter(); + value.WriteTo(valueWriter); + var valueBuf = valueWriter.ToArray(); + var valuePin = GCHandle.Alloc(valueBuf, GCHandleType.Pinned); + try + { + var result = NativeMethods.weaveffi_edge_cases_keywords_roundtrip(valuePin.AddrOfPinnedObject(), (UIntPtr)valueBuf.Length, out var outLen, ref err); + WeaveFFIError.Check(err); + var resultBuf = new byte[(int)outLen]; + if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); + NativeMethods.weaveffi_free_bytes(result, outLen); + var decodedReader = new WeaveFFIBufferReader(resultBuf); + var decoded = Keywords.ReadFrom(decodedReader); + decodedReader.ExpectEnd(); + return decoded; + } + finally + { + valuePin.Free(); + } + } + + [Obsolete("Use Mode-based APIs")] + public static Retired LegacyValue(Legacy value) + { + var err = new WeaveFFIError(); + var result = NativeMethods.weaveffi_edge_cases_legacy_value((int)value, out var outLen, ref err); + WeaveFFIError.Check(err); + var resultBuf = new byte[(int)outLen]; + if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); + NativeMethods.weaveffi_free_bytes(result, outLen); + var decodedReader = new WeaveFFIBufferReader(resultBuf); + var decoded = Retired.ReadFrom(decodedReader); + decodedReader.ExpectEnd(); + return decoded; + } + + } + +} + +// END WeaveFFI.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_async_demo__WeaveFFI_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__WeaveFFI_csproj.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/dotnet_async_demo__WeaveFFI_csproj.snap rename to crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__WeaveFFI_csproj.snap diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_async_demo__WeaveFFI_nuspec.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__WeaveFFI_nuspec.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/dotnet_async_demo__WeaveFFI_nuspec.snap rename to crates/weaveffi-cli/tests/snapshots/dotnet_edge_cases__WeaveFFI_nuspec.snap diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_events__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_events__README_md.snap deleted file mode 100644 index 96a3a5e8..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_events__README_md.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI (.NET) - -Auto-generated P/Invoke bindings for the WeaveFFI native library. - -## Build - -```bash -dotnet build -``` - -## Pack - -```bash -dotnet pack -``` - -The resulting `.nupkg` will be in `bin/Debug/` (or `bin/Release/` with `-c Release`). - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_events__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_events__WeaveFFI_cs.snap deleted file mode 100644 index 2cf1cdc2..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_events__WeaveFFI_cs.snap +++ /dev/null @@ -1,503 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace WeaveFFI -{ - public class WeaveFFIException : Exception - { - public int Code { get; } - - public WeaveFFIException(int code, string message) : base(message) - { - Code = code; - } - } - - [StructLayout(LayoutKind.Sequential)] - internal struct WeaveFFIError - { - public int Code; - public IntPtr Message; - public IntPtr PayloadPtr; - public UIntPtr PayloadLen; - - internal static byte[]? CopyPayload(WeaveFFIError err) - { - if (err.PayloadPtr == IntPtr.Zero || (int)err.PayloadLen == 0) - { - return null; - } - var payload = new byte[(int)err.PayloadLen]; - Marshal.Copy(err.PayloadPtr, payload, 0, (int)err.PayloadLen); - return payload; - } - - internal static void Check(WeaveFFIError err) - { - if (err.Code != 0) - { - var code = err.Code; - var msg = Marshal.PtrToStringUTF8(err.Message) ?? ""; - NativeMethods.weaveffi_error_clear(ref err); - throw new WeaveFFIException(code, msg); - } - } - } - - internal static class WeaveFFIHelpers - { - internal static IntPtr StringToPtr(string? s) - { - return s == null ? IntPtr.Zero : Marshal.StringToCoTaskMemUTF8(s); - } - - internal static string? PtrToString(IntPtr ptr) - { - return ptr == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(ptr); - } - - internal static void FreePtr(IntPtr ptr) - { - Marshal.FreeCoTaskMem(ptr); - } - } - - /// Serializes values into the WeaveFFI value-buffer wire - /// format (little-endian, packed). - internal sealed class WeaveFFIBufferWriter - { - private byte[] _buf = new byte[64]; - private int _len; - - private void Ensure(int extra) - { - if (_len + extra <= _buf.Length) - { - return; - } - var size = _buf.Length * 2; - while (size < _len + extra) - { - size *= 2; - } - Array.Resize(ref _buf, size); - } - - internal void WriteBool(bool v) - { - Ensure(1); - _buf[_len++] = v ? (byte)1 : (byte)0; - } - - internal void WriteI8(sbyte v) - { - Ensure(1); - _buf[_len++] = (byte)v; - } - - internal void WriteU8(byte v) - { - Ensure(1); - _buf[_len++] = v; - } - - internal void WriteU16(ushort v) - { - Ensure(2); - _buf[_len++] = (byte)v; - _buf[_len++] = (byte)(v >> 8); - } - - internal void WriteI16(short v) - { - WriteU16((ushort)v); - } - - internal void WriteU32(uint v) - { - Ensure(4); - _buf[_len++] = (byte)v; - _buf[_len++] = (byte)(v >> 8); - _buf[_len++] = (byte)(v >> 16); - _buf[_len++] = (byte)(v >> 24); - } - - internal void WriteI32(int v) - { - WriteU32((uint)v); - } - - internal void WriteU64(ulong v) - { - WriteU32((uint)v); - WriteU32((uint)(v >> 32)); - } - - internal void WriteI64(long v) - { - WriteU64((ulong)v); - } - - internal void WriteF32(float v) - { - WriteU32((uint)BitConverter.SingleToInt32Bits(v)); - } - - internal void WriteF64(double v) - { - WriteU64((ulong)BitConverter.DoubleToInt64Bits(v)); - } - - internal void WriteLen(int len) - { - WriteU32((uint)len); - } - - internal void WriteOptionFlag(bool present) - { - WriteBool(present); - } - - internal void WriteString(string v) - { - var bytes = System.Text.Encoding.UTF8.GetBytes(v); - WriteLen(bytes.Length); - Ensure(bytes.Length); - Array.Copy(bytes, 0, _buf, _len, bytes.Length); - _len += bytes.Length; - } - - internal void WriteBytes(byte[] v) - { - WriteLen(v.Length); - Ensure(v.Length); - Array.Copy(v, 0, _buf, _len, v.Length); - _len += v.Length; - } - - internal byte[] ToArray() - { - var outBuf = new byte[_len]; - Array.Copy(_buf, outBuf, _len); - return outBuf; - } - } - - /// Decodes values from the WeaveFFI value-buffer wire format. - /// A malformed buffer indicates a producer/consumer contract violation and - /// throws . - internal sealed class WeaveFFIBufferReader - { - private static readonly System.Text.Encoding Utf8Strict = - new System.Text.UTF8Encoding(false, true); - - private readonly byte[] _data; - private int _pos; - - internal WeaveFFIBufferReader(byte[] data) - { - _data = data; - } - - private void Require(int n) - { - if (_data.Length - _pos < n) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: buffer exhausted"); - } - } - - internal bool ReadBool() - { - Require(1); - var b = _data[_pos++]; - if (b > 1) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: invalid bool byte"); - } - return b == 1; - } - - internal sbyte ReadI8() - { - Require(1); - return (sbyte)_data[_pos++]; - } - - internal byte ReadU8() - { - Require(1); - return _data[_pos++]; - } - - internal ushort ReadU16() - { - Require(2); - var v = (ushort)(_data[_pos] | (_data[_pos + 1] << 8)); - _pos += 2; - return v; - } - - internal short ReadI16() - { - return (short)ReadU16(); - } - - internal uint ReadU32() - { - Require(4); - var v = (uint)_data[_pos] - | ((uint)_data[_pos + 1] << 8) - | ((uint)_data[_pos + 2] << 16) - | ((uint)_data[_pos + 3] << 24); - _pos += 4; - return v; - } - - internal int ReadI32() - { - return (int)ReadU32(); - } - - internal ulong ReadU64() - { - var lo = (ulong)ReadU32(); - var hi = (ulong)ReadU32(); - return lo | (hi << 32); - } - - internal long ReadI64() - { - return (long)ReadU64(); - } - - internal float ReadF32() - { - return BitConverter.Int32BitsToSingle(ReadI32()); - } - - internal double ReadF64() - { - return BitConverter.Int64BitsToDouble(ReadI64()); - } - - internal int ReadLen() - { - var len = ReadU32(); - if (len > (uint)(_data.Length - _pos)) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: length prefix exceeds remaining bytes"); - } - return (int)len; - } - - internal bool ReadOptionFlag() - { - return ReadBool(); - } - - internal string ReadString() - { - var len = ReadLen(); - string s; - try - { - s = Utf8Strict.GetString(_data, _pos, len); - } - catch (ArgumentException) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: string is not valid UTF-8"); - } - _pos += len; - return s; - } - - internal byte[] ReadBytes() - { - var len = ReadLen(); - var outBuf = new byte[len]; - Array.Copy(_data, _pos, outBuf, 0, len); - _pos += len; - return outBuf; - } - - internal void ExpectEnd() - { - if (_pos != _data.Length) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: trailing bytes"); - } - } - } - - /// A lazily streamed sequence backed by a native iterator. - /// It can be enumerated exactly once; enumerate it promptly (or call - /// a materializing operator such as ToList) and let the enumerator be - /// disposed to release the native iterator. - internal sealed class WeaveFFIOnceEnumerable : IEnumerable - { - private IEnumerator? _enumerator; - - internal WeaveFFIOnceEnumerable(IEnumerator enumerator) - { - _enumerator = enumerator; - } - - public IEnumerator GetEnumerator() - { - var e = System.Threading.Interlocked.Exchange(ref _enumerator, null); - if (e == null) - { - throw new InvalidOperationException("this sequence can be enumerated only once"); - } - return e; - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } - - internal static class NativeMethods - { - private const string LibName = "weaveffi"; - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_free_string(IntPtr ptr); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_free_bytes(IntPtr ptr, UIntPtr len); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_clear(ref WeaveFFIError err); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_free(IntPtr err); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - internal delegate void Cb_weaveffi_events_OnMessage_fn(IntPtr message, IntPtr context); - - [DllImport(LibName, EntryPoint = "weaveffi_events_register_message_listener", CallingConvention = CallingConvention.Cdecl)] - internal static extern ulong weaveffi_events_register_message_listener(Cb_weaveffi_events_OnMessage_fn callback, IntPtr context); - - [DllImport(LibName, EntryPoint = "weaveffi_events_unregister_message_listener", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_events_unregister_message_listener(ulong id); - - [DllImport(LibName, EntryPoint = "weaveffi_events_send_message", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_events_send_message(IntPtr text, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_events_get_messages", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_events_get_messages(ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_events_GetMessagesIterator_next", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_events_GetMessagesIterator_next(IntPtr iter, out IntPtr out_item, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_events_GetMessagesIterator_destroy", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_events_GetMessagesIterator_destroy(IntPtr iter); - - } - - public static class Events - { - private static readonly object _listenerLock = new object(); - // Live listener delegates by subscription id. Holding the delegate - // here keeps its native thunk alive until unregistered; without this - // the GC could collect a delegate the producer still calls. - private static readonly Dictionary _listenerRefs = new Dictionary(); - - /// A subscription id for UnregisterMessageListener(). - public static ulong RegisterMessageListener(Action callback) - { - NativeMethods.Cb_weaveffi_events_OnMessage_fn trampoline = (message, context) => - { - callback(Marshal.PtrToStringUTF8(message) ?? ""); - }; - ulong id; - lock (_listenerLock) - { - id = NativeMethods.weaveffi_events_register_message_listener(trampoline, IntPtr.Zero); - _listenerRefs[id] = trampoline; - } - return id; - } - - /// Unregisters a listener previously registered with RegisterMessageListener(). - public static void UnregisterMessageListener(ulong id) - { - NativeMethods.weaveffi_events_unregister_message_listener(id); - lock (_listenerLock) - { - _listenerRefs.Remove(id); - } - } - - /// Send a message, triggering the OnMessage callback - public static void SendMessage(string text) - { - var err = new WeaveFFIError(); - var textPtr = Marshal.StringToCoTaskMemUTF8(text); - try - { - NativeMethods.weaveffi_events_send_message(textPtr, ref err); - WeaveFFIError.Check(err); - } - finally - { - Marshal.FreeCoTaskMem(textPtr); - } - } - - /// Return an iterator over all sent messages - /// Streams lazily: each element is pulled from the native - /// iterator on demand, and the iterator is destroyed when enumeration - /// completes or the enumerator is disposed (a foreach disposes it - /// automatically, including on early exit). The returned sequence can be - /// enumerated only once. - public static IEnumerable GetMessages() - { - var err = new WeaveFFIError(); - var iter = NativeMethods.weaveffi_events_get_messages(ref err); - WeaveFFIError.Check(err); - return new WeaveFFIOnceEnumerable(EnumerateGetMessages(iter)); - } - - private static IEnumerator EnumerateGetMessages(IntPtr iter) - { - try - { - while (true) - { - var iterErr = new WeaveFFIError(); - if (NativeMethods.weaveffi_events_GetMessagesIterator_next(iter, out var out_item, ref iterErr) == 0) - { - WeaveFFIError.Check(iterErr); - yield break; - } - WeaveFFIError.Check(iterErr); - var item = Marshal.PtrToStringUTF8(out_item) ?? ""; - NativeMethods.weaveffi_free_string(out_item); - yield return item; - } - } - finally - { - NativeMethods.weaveffi_events_GetMessagesIterator_destroy(iter); - } - } - - } - -} - -// END WeaveFFI.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_events__WeaveFFI_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_events__WeaveFFI_csproj.snap deleted file mode 100644 index 97f37904..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_events__WeaveFFI_csproj.snap +++ /dev/null @@ -1,20 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - - - - - net8.0 - WeaveFFI - 0.1.0 - WeaveFFI bindings generated by WeaveFFI - true - - - - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_events__WeaveFFI_nuspec.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_events__WeaveFFI_nuspec.snap deleted file mode 100644 index 55db5121..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_events__WeaveFFI_nuspec.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - - - - - WeaveFFI - 0.1.0 - WeaveFFI Contributors - WeaveFFI bindings generated by WeaveFFI - MIT - https://github.com/weavefoundry/weaveffi - ffi interop native pinvoke - - - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_inventory__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_inventory__README_md.snap deleted file mode 100644 index 96a3a5e8..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_inventory__README_md.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI (.NET) - -Auto-generated P/Invoke bindings for the WeaveFFI native library. - -## Build - -```bash -dotnet build -``` - -## Pack - -```bash -dotnet pack -``` - -The resulting `.nupkg` will be in `bin/Debug/` (or `bin/Release/` with `-c Release`). - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_inventory__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_inventory__WeaveFFI_cs.snap deleted file mode 100644 index d9e994b5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_inventory__WeaveFFI_cs.snap +++ /dev/null @@ -1,818 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace WeaveFFI -{ - public class WeaveFFIException : Exception - { - public int Code { get; } - - public WeaveFFIException(int code, string message) : base(message) - { - Code = code; - } - } - - /// Typed exception for the ProductsError error domain (module products). - public class ProductsException : WeaveFFIException - { - /// price must be positive - public const int InvalidPrice = 1; - /// product not found - public const int ProductNotFound = 2; - - public ProductsException(int code, string message) : base(code, message) - { - } - - /// Wraps a raw error slot in the typed exception, falling - /// back to for unknown codes. Codes - /// declaring payload fields decode them into Data. - internal static WeaveFFIException FromCode(int code, string message, byte[]? payload) - { - switch (code) - { - case InvalidPrice: - return new ProductsException(code, string.IsNullOrEmpty(message) ? "price must be positive" : message); - case ProductNotFound: - return new ProductsException(code, string.IsNullOrEmpty(message) ? "product not found" : message); - default: - return new WeaveFFIException(code, message); - } - } - } - - /// Typed exception for the OrdersError error domain (module orders). - public class OrdersException : WeaveFFIException - { - /// order not found - public const int OrderNotFound = 1; - /// order must contain at least one item - public const int EmptyOrder = 2; - - public OrdersException(int code, string message) : base(code, message) - { - } - - /// Wraps a raw error slot in the typed exception, falling - /// back to for unknown codes. Codes - /// declaring payload fields decode them into Data. - internal static WeaveFFIException FromCode(int code, string message, byte[]? payload) - { - switch (code) - { - case OrderNotFound: - return new OrdersException(code, string.IsNullOrEmpty(message) ? "order not found" : message); - case EmptyOrder: - return new OrdersException(code, string.IsNullOrEmpty(message) ? "order must contain at least one item" : message); - default: - return new WeaveFFIException(code, message); - } - } - } - - [StructLayout(LayoutKind.Sequential)] - internal struct WeaveFFIError - { - public int Code; - public IntPtr Message; - public IntPtr PayloadPtr; - public UIntPtr PayloadLen; - - internal static byte[]? CopyPayload(WeaveFFIError err) - { - if (err.PayloadPtr == IntPtr.Zero || (int)err.PayloadLen == 0) - { - return null; - } - var payload = new byte[(int)err.PayloadLen]; - Marshal.Copy(err.PayloadPtr, payload, 0, (int)err.PayloadLen); - return payload; - } - - internal static void Check(WeaveFFIError err) - { - if (err.Code != 0) - { - var code = err.Code; - var msg = Marshal.PtrToStringUTF8(err.Message) ?? ""; - NativeMethods.weaveffi_error_clear(ref err); - throw new WeaveFFIException(code, msg); - } - } - - internal static void CheckProducts(WeaveFFIError err) - { - if (err.Code != 0) - { - var code = err.Code; - var msg = Marshal.PtrToStringUTF8(err.Message) ?? ""; - var payload = CopyPayload(err); - NativeMethods.weaveffi_error_clear(ref err); - throw ProductsException.FromCode(code, msg, payload); - } - } - - internal static void CheckOrders(WeaveFFIError err) - { - if (err.Code != 0) - { - var code = err.Code; - var msg = Marshal.PtrToStringUTF8(err.Message) ?? ""; - var payload = CopyPayload(err); - NativeMethods.weaveffi_error_clear(ref err); - throw OrdersException.FromCode(code, msg, payload); - } - } - } - - internal static class WeaveFFIHelpers - { - internal static IntPtr StringToPtr(string? s) - { - return s == null ? IntPtr.Zero : Marshal.StringToCoTaskMemUTF8(s); - } - - internal static string? PtrToString(IntPtr ptr) - { - return ptr == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(ptr); - } - - internal static void FreePtr(IntPtr ptr) - { - Marshal.FreeCoTaskMem(ptr); - } - } - - /// Serializes values into the WeaveFFI value-buffer wire - /// format (little-endian, packed). - internal sealed class WeaveFFIBufferWriter - { - private byte[] _buf = new byte[64]; - private int _len; - - private void Ensure(int extra) - { - if (_len + extra <= _buf.Length) - { - return; - } - var size = _buf.Length * 2; - while (size < _len + extra) - { - size *= 2; - } - Array.Resize(ref _buf, size); - } - - internal void WriteBool(bool v) - { - Ensure(1); - _buf[_len++] = v ? (byte)1 : (byte)0; - } - - internal void WriteI8(sbyte v) - { - Ensure(1); - _buf[_len++] = (byte)v; - } - - internal void WriteU8(byte v) - { - Ensure(1); - _buf[_len++] = v; - } - - internal void WriteU16(ushort v) - { - Ensure(2); - _buf[_len++] = (byte)v; - _buf[_len++] = (byte)(v >> 8); - } - - internal void WriteI16(short v) - { - WriteU16((ushort)v); - } - - internal void WriteU32(uint v) - { - Ensure(4); - _buf[_len++] = (byte)v; - _buf[_len++] = (byte)(v >> 8); - _buf[_len++] = (byte)(v >> 16); - _buf[_len++] = (byte)(v >> 24); - } - - internal void WriteI32(int v) - { - WriteU32((uint)v); - } - - internal void WriteU64(ulong v) - { - WriteU32((uint)v); - WriteU32((uint)(v >> 32)); - } - - internal void WriteI64(long v) - { - WriteU64((ulong)v); - } - - internal void WriteF32(float v) - { - WriteU32((uint)BitConverter.SingleToInt32Bits(v)); - } - - internal void WriteF64(double v) - { - WriteU64((ulong)BitConverter.DoubleToInt64Bits(v)); - } - - internal void WriteLen(int len) - { - WriteU32((uint)len); - } - - internal void WriteOptionFlag(bool present) - { - WriteBool(present); - } - - internal void WriteString(string v) - { - var bytes = System.Text.Encoding.UTF8.GetBytes(v); - WriteLen(bytes.Length); - Ensure(bytes.Length); - Array.Copy(bytes, 0, _buf, _len, bytes.Length); - _len += bytes.Length; - } - - internal void WriteBytes(byte[] v) - { - WriteLen(v.Length); - Ensure(v.Length); - Array.Copy(v, 0, _buf, _len, v.Length); - _len += v.Length; - } - - internal byte[] ToArray() - { - var outBuf = new byte[_len]; - Array.Copy(_buf, outBuf, _len); - return outBuf; - } - } - - /// Decodes values from the WeaveFFI value-buffer wire format. - /// A malformed buffer indicates a producer/consumer contract violation and - /// throws . - internal sealed class WeaveFFIBufferReader - { - private static readonly System.Text.Encoding Utf8Strict = - new System.Text.UTF8Encoding(false, true); - - private readonly byte[] _data; - private int _pos; - - internal WeaveFFIBufferReader(byte[] data) - { - _data = data; - } - - private void Require(int n) - { - if (_data.Length - _pos < n) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: buffer exhausted"); - } - } - - internal bool ReadBool() - { - Require(1); - var b = _data[_pos++]; - if (b > 1) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: invalid bool byte"); - } - return b == 1; - } - - internal sbyte ReadI8() - { - Require(1); - return (sbyte)_data[_pos++]; - } - - internal byte ReadU8() - { - Require(1); - return _data[_pos++]; - } - - internal ushort ReadU16() - { - Require(2); - var v = (ushort)(_data[_pos] | (_data[_pos + 1] << 8)); - _pos += 2; - return v; - } - - internal short ReadI16() - { - return (short)ReadU16(); - } - - internal uint ReadU32() - { - Require(4); - var v = (uint)_data[_pos] - | ((uint)_data[_pos + 1] << 8) - | ((uint)_data[_pos + 2] << 16) - | ((uint)_data[_pos + 3] << 24); - _pos += 4; - return v; - } - - internal int ReadI32() - { - return (int)ReadU32(); - } - - internal ulong ReadU64() - { - var lo = (ulong)ReadU32(); - var hi = (ulong)ReadU32(); - return lo | (hi << 32); - } - - internal long ReadI64() - { - return (long)ReadU64(); - } - - internal float ReadF32() - { - return BitConverter.Int32BitsToSingle(ReadI32()); - } - - internal double ReadF64() - { - return BitConverter.Int64BitsToDouble(ReadI64()); - } - - internal int ReadLen() - { - var len = ReadU32(); - if (len > (uint)(_data.Length - _pos)) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: length prefix exceeds remaining bytes"); - } - return (int)len; - } - - internal bool ReadOptionFlag() - { - return ReadBool(); - } - - internal string ReadString() - { - var len = ReadLen(); - string s; - try - { - s = Utf8Strict.GetString(_data, _pos, len); - } - catch (ArgumentException) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: string is not valid UTF-8"); - } - _pos += len; - return s; - } - - internal byte[] ReadBytes() - { - var len = ReadLen(); - var outBuf = new byte[len]; - Array.Copy(_data, _pos, outBuf, 0, len); - _pos += len; - return outBuf; - } - - internal void ExpectEnd() - { - if (_pos != _data.Length) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: trailing bytes"); - } - } - } - - public enum Category - { - Electronics = 0, - Clothing = 1, - Food = 2, - Books = 3, - } - - public sealed class Product - { - public long Id { get; } - - public string Name { get; } - - public string? Description { get; } - - public double Price { get; } - - public Category Category { get; } - - public string[] Tags { get; } - - public Product(long id, string name, string? description, double price, Category category, string[] tags) - { - Id = id; - Name = name; - Description = description; - Price = price; - Category = category; - Tags = tags; - } - - internal void WriteTo(WeaveFFIBufferWriter writer) - { - writer.WriteI64(Id); - writer.WriteString(Name); - if (Description != null) - { - writer.WriteOptionFlag(true); - writer.WriteString(Description!); - } - else - { - writer.WriteOptionFlag(false); - } - writer.WriteF64(Price); - writer.WriteI32((int)Category); - writer.WriteLen(Tags.Length); - foreach (var item0 in Tags) - { - writer.WriteString(item0); - } - } - - internal static Product ReadFrom(WeaveFFIBufferReader reader) - { - var fId = reader.ReadI64(); - var fName = reader.ReadString(); - string? fDescription = null; - if (reader.ReadOptionFlag()) - { - var fDescriptionValue = reader.ReadString(); - fDescription = fDescriptionValue; - } - var fPrice = reader.ReadF64(); - var fCategory = (Category)reader.ReadI32(); - var fTagsCount = reader.ReadLen(); - var fTags = new string[fTagsCount]; - for (int i0 = 0; i0 < fTagsCount; i0++) - { - var fTagsItem = reader.ReadString(); - fTags[i0] = fTagsItem; - } - return new Product(fId, fName, fDescription, fPrice, fCategory, fTags); - } - } - - /// A product catalog owning its products - public class Catalog : IDisposable - { - private IntPtr _handle; - private bool _disposed; - - internal Catalog(IntPtr handle) - { - _handle = handle; - } - - internal IntPtr Handle => _handle; - - public Catalog() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_products_Catalog_new(ref err); - WeaveFFIError.Check(err); - _handle = result; - } - - /// Thrown when the call reports a ProductsError code. - public Product AddProduct(string name, double price, Category category) - { - var err = new WeaveFFIError(); - var namePtr = Marshal.StringToCoTaskMemUTF8(name); - try - { - var result = NativeMethods.weaveffi_products_Catalog_add_product(_handle, namePtr, price, (int)category, out var outLen, ref err); - WeaveFFIError.CheckProducts(err); - var resultBuf = new byte[(int)outLen]; - if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); - NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var value = Product.ReadFrom(valueReader); - valueReader.ExpectEnd(); - return value; - } - finally - { - Marshal.FreeCoTaskMem(namePtr); - } - } - - /// Thrown when the call reports a ProductsError code. - public Product GetProduct(long id) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_products_Catalog_get_product(_handle, id, out var outLen, ref err); - WeaveFFIError.CheckProducts(err); - var resultBuf = new byte[(int)outLen]; - if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); - NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var value = Product.ReadFrom(valueReader); - valueReader.ExpectEnd(); - return value; - } - - public Product[] Search(Category category) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_products_Catalog_search(_handle, (int)category, out var outLen, ref err); - WeaveFFIError.Check(err); - var resultBuf = new byte[(int)outLen]; - if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); - NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var valueCount = valueReader.ReadLen(); - var value = new Product[valueCount]; - for (int i0 = 0; i0 < valueCount; i0++) - { - var valueItem = Product.ReadFrom(valueReader); - value[i0] = valueItem; - } - valueReader.ExpectEnd(); - return value; - } - - /// Thrown when the call reports a ProductsError code. - public bool UpdatePrice(long id, double price) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_products_Catalog_update_price(_handle, id, price, ref err); - WeaveFFIError.CheckProducts(err); - return result != 0; - } - - public bool Remove(long id) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_products_Catalog_remove(_handle, id, ref err); - WeaveFFIError.Check(err); - return result != 0; - } - - public void Dispose() - { - if (!_disposed) - { - NativeMethods.weaveffi_products_Catalog_destroy(_handle); - _disposed = true; - } - GC.SuppressFinalize(this); - } - - ~Catalog() - { - Dispose(); - } - } - - public sealed class OrderItem - { - public long ProductId { get; } - - public int Quantity { get; } - - public double UnitPrice { get; } - - public OrderItem(long productId, int quantity, double unitPrice) - { - ProductId = productId; - Quantity = quantity; - UnitPrice = unitPrice; - } - - internal void WriteTo(WeaveFFIBufferWriter writer) - { - writer.WriteI64(ProductId); - writer.WriteI32(Quantity); - writer.WriteF64(UnitPrice); - } - - internal static OrderItem ReadFrom(WeaveFFIBufferReader reader) - { - var fProductId = reader.ReadI64(); - var fQuantity = reader.ReadI32(); - var fUnitPrice = reader.ReadF64(); - return new OrderItem(fProductId, fQuantity, fUnitPrice); - } - } - - public sealed class Order - { - public long Id { get; } - - public OrderItem[] Items { get; } - - public double Total { get; } - - public string Status { get; } - - public Order(long id, OrderItem[] items, double total, string status) - { - Id = id; - Items = items; - Total = total; - Status = status; - } - - internal void WriteTo(WeaveFFIBufferWriter writer) - { - writer.WriteI64(Id); - writer.WriteLen(Items.Length); - foreach (var item0 in Items) - { - item0.WriteTo(writer); - } - writer.WriteF64(Total); - writer.WriteString(Status); - } - - internal static Order ReadFrom(WeaveFFIBufferReader reader) - { - var fId = reader.ReadI64(); - var fItemsCount = reader.ReadLen(); - var fItems = new OrderItem[fItemsCount]; - for (int i0 = 0; i0 < fItemsCount; i0++) - { - var fItemsItem = OrderItem.ReadFrom(reader); - fItems[i0] = fItemsItem; - } - var fTotal = reader.ReadF64(); - var fStatus = reader.ReadString(); - return new Order(fId, fItems, fTotal, fStatus); - } - } - - internal static class NativeMethods - { - private const string LibName = "weaveffi"; - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_free_string(IntPtr ptr); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_free_bytes(IntPtr ptr, UIntPtr len); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_clear(ref WeaveFFIError err); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_free(IntPtr err); - - [DllImport(LibName, EntryPoint = "weaveffi_products_Catalog_destroy", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_products_Catalog_destroy(IntPtr self); - - [DllImport(LibName, EntryPoint = "weaveffi_products_Catalog_new", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_products_Catalog_new(ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_products_Catalog_add_product", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_products_Catalog_add_product(IntPtr self, IntPtr name, double price, int category, out UIntPtr out_len, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_products_Catalog_get_product", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_products_Catalog_get_product(IntPtr self, long id, out UIntPtr out_len, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_products_Catalog_search", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_products_Catalog_search(IntPtr self, int category, out UIntPtr out_len, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_products_Catalog_update_price", CallingConvention = CallingConvention.Cdecl)] - internal static extern byte weaveffi_products_Catalog_update_price(IntPtr self, long id, double price, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_products_Catalog_remove", CallingConvention = CallingConvention.Cdecl)] - internal static extern byte weaveffi_products_Catalog_remove(IntPtr self, long id, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_orders_create_order", CallingConvention = CallingConvention.Cdecl)] - internal static extern long weaveffi_orders_create_order(IntPtr items_ptr, UIntPtr items_len, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_orders_get_order", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_orders_get_order(long id, out UIntPtr out_len, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_orders_cancel_order", CallingConvention = CallingConvention.Cdecl)] - internal static extern byte weaveffi_orders_cancel_order(long id, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_orders_add_product_to_order", CallingConvention = CallingConvention.Cdecl)] - internal static extern byte weaveffi_orders_add_product_to_order(long order_id, IntPtr product_ptr, UIntPtr product_len, ref WeaveFFIError err); - - } - - public static class Products - { - } - - public static class Orders - { - /// Thrown when the call reports a OrdersError code. - public static long CreateOrder(OrderItem[] items) - { - var err = new WeaveFFIError(); - var itemsWriter = new WeaveFFIBufferWriter(); - itemsWriter.WriteLen(items.Length); - foreach (var item0 in items) - { - item0.WriteTo(itemsWriter); - } - var itemsBuf = itemsWriter.ToArray(); - var itemsPin = GCHandle.Alloc(itemsBuf, GCHandleType.Pinned); - try - { - var result = NativeMethods.weaveffi_orders_create_order(itemsPin.AddrOfPinnedObject(), (UIntPtr)itemsBuf.Length, ref err); - WeaveFFIError.CheckOrders(err); - return result; - } - finally - { - itemsPin.Free(); - } - } - - /// Thrown when the call reports a OrdersError code. - public static Order GetOrder(long id) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_orders_get_order(id, out var outLen, ref err); - WeaveFFIError.CheckOrders(err); - var resultBuf = new byte[(int)outLen]; - if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); - NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var value = Order.ReadFrom(valueReader); - valueReader.ExpectEnd(); - return value; - } - - public static bool CancelOrder(long id) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_orders_cancel_order(id, ref err); - WeaveFFIError.Check(err); - return result != 0; - } - - public static bool AddProductToOrder(long orderId, Product product) - { - var err = new WeaveFFIError(); - var productWriter = new WeaveFFIBufferWriter(); - product.WriteTo(productWriter); - var productBuf = productWriter.ToArray(); - var productPin = GCHandle.Alloc(productBuf, GCHandleType.Pinned); - try - { - var result = NativeMethods.weaveffi_orders_add_product_to_order(orderId, productPin.AddrOfPinnedObject(), (UIntPtr)productBuf.Length, ref err); - WeaveFFIError.Check(err); - return result != 0; - } - finally - { - productPin.Free(); - } - } - - } - -} - -// END WeaveFFI.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_inventory__WeaveFFI_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_inventory__WeaveFFI_csproj.snap deleted file mode 100644 index 97f37904..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_inventory__WeaveFFI_csproj.snap +++ /dev/null @@ -1,20 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - - - - - net8.0 - WeaveFFI - 0.1.0 - WeaveFFI bindings generated by WeaveFFI - true - - - - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_inventory__WeaveFFI_nuspec.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_inventory__WeaveFFI_nuspec.snap deleted file mode 100644 index 55db5121..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_inventory__WeaveFFI_nuspec.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - - - - - WeaveFFI - 0.1.0 - WeaveFFI Contributors - WeaveFFI bindings generated by WeaveFFI - MIT - https://github.com/weavefoundry/weaveffi - ffi interop native pinvoke - - - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__WeaveFFI_cs.snap index 8f792b31..3e5e36a4 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__WeaveFFI_cs.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_kitchen_sink__WeaveFFI_cs.snap @@ -652,6 +652,34 @@ namespace WeaveFFI { private const string LibName = "weaveffi"; + // The ABI revision these bindings were generated against. + internal const uint AbiVersion = 1; + + // Runs before the first P/Invoke through this class, so a producer built + // for a different ABI revision fails loudly instead of misreading the + // error struct or a value buffer later. + static NativeMethods() + { + uint found; + try + { + found = weaveffi_abi_version(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded WeaveFFI library predates ABI versioning (these bindings expect ABI revision {AbiVersion})", e); + } + if (found != AbiVersion) + { + throw new InvalidOperationException( + $"WeaveFFI ABI mismatch: these bindings expect revision {AbiVersion} but the loaded library reports revision {found}"); + } + } + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern uint weaveffi_abi_version(); + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] internal static extern void weaveffi_free_string(IntPtr ptr); @@ -965,15 +993,15 @@ namespace WeaveFFI var resultBuf = new byte[(int)outLen]; if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - Item? value = null; - if (valueReader.ReadOptionFlag()) + var decodedReader = new WeaveFFIBufferReader(resultBuf); + Item? decoded = null; + if (decodedReader.ReadOptionFlag()) { - var valueValue = Item.ReadFrom(valueReader); - value = valueValue; + var decodedValue = Item.ReadFrom(decodedReader); + decoded = decodedValue; } - valueReader.ExpectEnd(); - return value; + decodedReader.ExpectEnd(); + return decoded; } /// List of structs @@ -985,16 +1013,16 @@ namespace WeaveFFI var resultBuf = new byte[(int)outLen]; if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var valueCount = valueReader.ReadLen(); - var value = new Item[valueCount]; - for (int i0 = 0; i0 < valueCount; i0++) + var decodedReader = new WeaveFFIBufferReader(resultBuf); + var decodedCount = decodedReader.ReadLen(); + var decoded = new Item[decodedCount]; + for (int i0 = 0; i0 < decodedCount; i0++) { - var valueItem = Item.ReadFrom(valueReader); - value[i0] = valueItem; + var decodedItem = Item.ReadFrom(decodedReader); + decoded[i0] = decodedItem; } - valueReader.ExpectEnd(); - return value; + decodedReader.ExpectEnd(); + return decoded; } /// Map return type @@ -1006,17 +1034,17 @@ namespace WeaveFFI var resultBuf = new byte[(int)outLen]; if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var valueCount = valueReader.ReadLen(); - var value = new Dictionary(valueCount); - for (int i0 = 0; i0 < valueCount; i0++) + var decodedReader = new WeaveFFIBufferReader(resultBuf); + var decodedCount = decodedReader.ReadLen(); + var decoded = new Dictionary(decodedCount); + for (int i0 = 0; i0 < decodedCount; i0++) { - var valueKey = valueReader.ReadString(); - var valueVal = valueReader.ReadI32(); - value[valueKey] = valueVal; + var decodedKey = decodedReader.ReadString(); + var decodedVal = decodedReader.ReadI32(); + decoded[decodedKey] = decodedVal; } - valueReader.ExpectEnd(); - return value; + decodedReader.ExpectEnd(); + return decoded; } /// Iterator return type @@ -1070,10 +1098,10 @@ namespace WeaveFFI var resultBuf = new byte[(int)outLen]; if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var value = Token.ReadFrom(valueReader); - valueReader.ExpectEnd(); - return value; + var decodedReader = new WeaveFFIBufferReader(resultBuf); + var decoded = Token.ReadFrom(decodedReader); + decodedReader.ExpectEnd(); + return decoded; } /// Async operation diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_kvstore__README_md.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kvstore__README_md.snap deleted file mode 100644 index 96a3a5e8..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_kvstore__README_md.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI (.NET) - -Auto-generated P/Invoke bindings for the WeaveFFI native library. - -## Build - -```bash -dotnet build -``` - -## Pack - -```bash -dotnet pack -``` - -The resulting `.nupkg` will be in `bin/Debug/` (or `bin/Release/` with `-c Release`). - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_kvstore__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kvstore__WeaveFFI_cs.snap deleted file mode 100644 index 7d2f1429..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_kvstore__WeaveFFI_cs.snap +++ /dev/null @@ -1,970 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Threading.Tasks; - -namespace WeaveFFI -{ - public class WeaveFFIException : Exception - { - public int Code { get; } - - public WeaveFFIException(int code, string message) : base(message) - { - Code = code; - } - } - - /// Typed exception for the KvError error domain (module kv). - public class KvException : WeaveFFIException - { - /// No entry exists for the requested key - public const int KeyNotFound = 1001; - /// The entry exists but its TTL has elapsed - public const int Expired = 1002; - /// The store has reached its capacity limit - public const int StoreFull = 1003; - /// Underlying storage reported an I/O failure - public const int IoError = 1004; - - public KvException(int code, string message) : base(code, message) - { - } - - /// Wraps a raw error slot in the typed exception, falling - /// back to for unknown codes. Codes - /// declaring payload fields decode them into Data. - internal static WeaveFFIException FromCode(int code, string message, byte[]? payload) - { - switch (code) - { - case KeyNotFound: - return new KvException(code, string.IsNullOrEmpty(message) ? "key not found" : message); - case Expired: - return new KvException(code, string.IsNullOrEmpty(message) ? "entry expired" : message); - case StoreFull: - return new KvException(code, string.IsNullOrEmpty(message) ? "store has reached capacity" : message); - case IoError: - return new KvException(code, string.IsNullOrEmpty(message) ? "I/O failure" : message); - default: - return new WeaveFFIException(code, message); - } - } - } - - [StructLayout(LayoutKind.Sequential)] - internal struct WeaveFFIError - { - public int Code; - public IntPtr Message; - public IntPtr PayloadPtr; - public UIntPtr PayloadLen; - - internal static byte[]? CopyPayload(WeaveFFIError err) - { - if (err.PayloadPtr == IntPtr.Zero || (int)err.PayloadLen == 0) - { - return null; - } - var payload = new byte[(int)err.PayloadLen]; - Marshal.Copy(err.PayloadPtr, payload, 0, (int)err.PayloadLen); - return payload; - } - - internal static void Check(WeaveFFIError err) - { - if (err.Code != 0) - { - var code = err.Code; - var msg = Marshal.PtrToStringUTF8(err.Message) ?? ""; - NativeMethods.weaveffi_error_clear(ref err); - throw new WeaveFFIException(code, msg); - } - } - - internal static void CheckKv(WeaveFFIError err) - { - if (err.Code != 0) - { - var code = err.Code; - var msg = Marshal.PtrToStringUTF8(err.Message) ?? ""; - var payload = CopyPayload(err); - NativeMethods.weaveffi_error_clear(ref err); - throw KvException.FromCode(code, msg, payload); - } - } - } - - internal static class WeaveFFIHelpers - { - internal static IntPtr StringToPtr(string? s) - { - return s == null ? IntPtr.Zero : Marshal.StringToCoTaskMemUTF8(s); - } - - internal static string? PtrToString(IntPtr ptr) - { - return ptr == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(ptr); - } - - internal static void FreePtr(IntPtr ptr) - { - Marshal.FreeCoTaskMem(ptr); - } - } - - /// Serializes values into the WeaveFFI value-buffer wire - /// format (little-endian, packed). - internal sealed class WeaveFFIBufferWriter - { - private byte[] _buf = new byte[64]; - private int _len; - - private void Ensure(int extra) - { - if (_len + extra <= _buf.Length) - { - return; - } - var size = _buf.Length * 2; - while (size < _len + extra) - { - size *= 2; - } - Array.Resize(ref _buf, size); - } - - internal void WriteBool(bool v) - { - Ensure(1); - _buf[_len++] = v ? (byte)1 : (byte)0; - } - - internal void WriteI8(sbyte v) - { - Ensure(1); - _buf[_len++] = (byte)v; - } - - internal void WriteU8(byte v) - { - Ensure(1); - _buf[_len++] = v; - } - - internal void WriteU16(ushort v) - { - Ensure(2); - _buf[_len++] = (byte)v; - _buf[_len++] = (byte)(v >> 8); - } - - internal void WriteI16(short v) - { - WriteU16((ushort)v); - } - - internal void WriteU32(uint v) - { - Ensure(4); - _buf[_len++] = (byte)v; - _buf[_len++] = (byte)(v >> 8); - _buf[_len++] = (byte)(v >> 16); - _buf[_len++] = (byte)(v >> 24); - } - - internal void WriteI32(int v) - { - WriteU32((uint)v); - } - - internal void WriteU64(ulong v) - { - WriteU32((uint)v); - WriteU32((uint)(v >> 32)); - } - - internal void WriteI64(long v) - { - WriteU64((ulong)v); - } - - internal void WriteF32(float v) - { - WriteU32((uint)BitConverter.SingleToInt32Bits(v)); - } - - internal void WriteF64(double v) - { - WriteU64((ulong)BitConverter.DoubleToInt64Bits(v)); - } - - internal void WriteLen(int len) - { - WriteU32((uint)len); - } - - internal void WriteOptionFlag(bool present) - { - WriteBool(present); - } - - internal void WriteString(string v) - { - var bytes = System.Text.Encoding.UTF8.GetBytes(v); - WriteLen(bytes.Length); - Ensure(bytes.Length); - Array.Copy(bytes, 0, _buf, _len, bytes.Length); - _len += bytes.Length; - } - - internal void WriteBytes(byte[] v) - { - WriteLen(v.Length); - Ensure(v.Length); - Array.Copy(v, 0, _buf, _len, v.Length); - _len += v.Length; - } - - internal byte[] ToArray() - { - var outBuf = new byte[_len]; - Array.Copy(_buf, outBuf, _len); - return outBuf; - } - } - - /// Decodes values from the WeaveFFI value-buffer wire format. - /// A malformed buffer indicates a producer/consumer contract violation and - /// throws . - internal sealed class WeaveFFIBufferReader - { - private static readonly System.Text.Encoding Utf8Strict = - new System.Text.UTF8Encoding(false, true); - - private readonly byte[] _data; - private int _pos; - - internal WeaveFFIBufferReader(byte[] data) - { - _data = data; - } - - private void Require(int n) - { - if (_data.Length - _pos < n) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: buffer exhausted"); - } - } - - internal bool ReadBool() - { - Require(1); - var b = _data[_pos++]; - if (b > 1) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: invalid bool byte"); - } - return b == 1; - } - - internal sbyte ReadI8() - { - Require(1); - return (sbyte)_data[_pos++]; - } - - internal byte ReadU8() - { - Require(1); - return _data[_pos++]; - } - - internal ushort ReadU16() - { - Require(2); - var v = (ushort)(_data[_pos] | (_data[_pos + 1] << 8)); - _pos += 2; - return v; - } - - internal short ReadI16() - { - return (short)ReadU16(); - } - - internal uint ReadU32() - { - Require(4); - var v = (uint)_data[_pos] - | ((uint)_data[_pos + 1] << 8) - | ((uint)_data[_pos + 2] << 16) - | ((uint)_data[_pos + 3] << 24); - _pos += 4; - return v; - } - - internal int ReadI32() - { - return (int)ReadU32(); - } - - internal ulong ReadU64() - { - var lo = (ulong)ReadU32(); - var hi = (ulong)ReadU32(); - return lo | (hi << 32); - } - - internal long ReadI64() - { - return (long)ReadU64(); - } - - internal float ReadF32() - { - return BitConverter.Int32BitsToSingle(ReadI32()); - } - - internal double ReadF64() - { - return BitConverter.Int64BitsToDouble(ReadI64()); - } - - internal int ReadLen() - { - var len = ReadU32(); - if (len > (uint)(_data.Length - _pos)) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: length prefix exceeds remaining bytes"); - } - return (int)len; - } - - internal bool ReadOptionFlag() - { - return ReadBool(); - } - - internal string ReadString() - { - var len = ReadLen(); - string s; - try - { - s = Utf8Strict.GetString(_data, _pos, len); - } - catch (ArgumentException) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: string is not valid UTF-8"); - } - _pos += len; - return s; - } - - internal byte[] ReadBytes() - { - var len = ReadLen(); - var outBuf = new byte[len]; - Array.Copy(_data, _pos, outBuf, 0, len); - _pos += len; - return outBuf; - } - - internal void ExpectEnd() - { - if (_pos != _data.Length) - { - throw new InvalidOperationException("malformed WeaveFFI value buffer: trailing bytes"); - } - } - } - - /// A lazily streamed sequence backed by a native iterator. - /// It can be enumerated exactly once; enumerate it promptly (or call - /// a materializing operator such as ToList) and let the enumerator be - /// disposed to release the native iterator. - internal sealed class WeaveFFIOnceEnumerable : IEnumerable - { - private IEnumerator? _enumerator; - - internal WeaveFFIOnceEnumerable(IEnumerator enumerator) - { - _enumerator = enumerator; - } - - public IEnumerator GetEnumerator() - { - var e = System.Threading.Interlocked.Exchange(ref _enumerator, null); - if (e == null) - { - throw new InvalidOperationException("this sequence can be enumerated only once"); - } - return e; - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - } - - /// Persistence semantics applied to a stored entry - public enum EntryKind - { - /// In-memory only; lost on close - Volatile = 0, - /// Flushed to durable storage - Persistent = 1, - /// Persistent and encrypted at rest - Encrypted = 2, - } - - /// A single key-value entry persisted in the store - public sealed class Entry - { - /// Stable monotonic identifier assigned on insert - public long Id { get; } - - /// UTF-8 lookup key - public string Key { get; } - - /// Opaque binary payload - public byte[] Value { get; } - - /// Unix-timestamp seconds when the entry was created - public long CreatedAt { get; } - - /// Optional unix-timestamp seconds at which the entry expires - public long? ExpiresAt { get; } - - /// Free-form labels attached to the entry - public string[] Tags { get; } - - /// Arbitrary string-valued metadata pairs - public Dictionary Metadata { get; } - - public Entry(long id, string key, byte[] value, long createdAt, long? expiresAt, string[] tags, Dictionary metadata) - { - Id = id; - Key = key; - Value = value; - CreatedAt = createdAt; - ExpiresAt = expiresAt; - Tags = tags; - Metadata = metadata; - } - - internal void WriteTo(WeaveFFIBufferWriter writer) - { - writer.WriteI64(Id); - writer.WriteString(Key); - writer.WriteBytes(Value); - writer.WriteI64(CreatedAt); - if (ExpiresAt != null) - { - writer.WriteOptionFlag(true); - writer.WriteI64(ExpiresAt.Value); - } - else - { - writer.WriteOptionFlag(false); - } - writer.WriteLen(Tags.Length); - foreach (var item0 in Tags) - { - writer.WriteString(item0); - } - writer.WriteLen(Metadata.Count); - foreach (var entry0 in Metadata) - { - writer.WriteString(entry0.Key); - writer.WriteString(entry0.Value); - } - } - - internal static Entry ReadFrom(WeaveFFIBufferReader reader) - { - var fId = reader.ReadI64(); - var fKey = reader.ReadString(); - var fValue = reader.ReadBytes(); - var fCreatedAt = reader.ReadI64(); - long? fExpiresAt = null; - if (reader.ReadOptionFlag()) - { - var fExpiresAtValue = reader.ReadI64(); - fExpiresAt = fExpiresAtValue; - } - var fTagsCount = reader.ReadLen(); - var fTags = new string[fTagsCount]; - for (int i0 = 0; i0 < fTagsCount; i0++) - { - var fTagsItem = reader.ReadString(); - fTags[i0] = fTagsItem; - } - var fMetadataCount = reader.ReadLen(); - var fMetadata = new Dictionary(fMetadataCount); - for (int i0 = 0; i0 < fMetadataCount; i0++) - { - var fMetadataKey = reader.ReadString(); - var fMetadataVal = reader.ReadString(); - fMetadata[fMetadataKey] = fMetadataVal; - } - return new Entry(fId, fKey, fValue, fCreatedAt, fExpiresAt, fTags, fMetadata); - } - } - - /// An in-memory key-value store owning its entries; destroying the object releases all of its state - public class Store : IDisposable - { - private IntPtr _handle; - private bool _disposed; - - internal Store(IntPtr handle) - { - _handle = handle; - } - - internal IntPtr Handle => _handle; - - /// Open (or create) a store backed by the given filesystem path - /// Thrown when the call reports a KvError code. - public static Store Open(string path) - { - var err = new WeaveFFIError(); - var pathPtr = Marshal.StringToCoTaskMemUTF8(path); - try - { - var result = NativeMethods.weaveffi_kv_Store_open(pathPtr, ref err); - WeaveFFIError.CheckKv(err); - return new Store(result); - } - finally - { - Marshal.FreeCoTaskMem(pathPtr); - } - } - - /// Insert or replace a value, returning true on success - /// Thrown when the call reports a KvError code. - public bool Put(string key, byte[] value, EntryKind kind, long? ttlSeconds) - { - var err = new WeaveFFIError(); - var keyPtr = Marshal.StringToCoTaskMemUTF8(key); - var valuePin = GCHandle.Alloc(value, GCHandleType.Pinned); - var ttlSecondsWriter = new WeaveFFIBufferWriter(); - if (ttlSeconds != null) - { - ttlSecondsWriter.WriteOptionFlag(true); - ttlSecondsWriter.WriteI64(ttlSeconds.Value); - } - else - { - ttlSecondsWriter.WriteOptionFlag(false); - } - var ttlSecondsBuf = ttlSecondsWriter.ToArray(); - var ttlSecondsPin = GCHandle.Alloc(ttlSecondsBuf, GCHandleType.Pinned); - try - { - var result = NativeMethods.weaveffi_kv_Store_put(_handle, keyPtr, valuePin.AddrOfPinnedObject(), (UIntPtr)value.Length, (int)kind, ttlSecondsPin.AddrOfPinnedObject(), (UIntPtr)ttlSecondsBuf.Length, ref err); - WeaveFFIError.CheckKv(err); - return result != 0; - } - finally - { - Marshal.FreeCoTaskMem(keyPtr); - valuePin.Free(); - ttlSecondsPin.Free(); - } - } - - /// Look up an entry by key; returns null if missing or expired - /// Thrown when the call reports a KvError code. - public Entry? Get(string key) - { - var err = new WeaveFFIError(); - var keyPtr = Marshal.StringToCoTaskMemUTF8(key); - try - { - var result = NativeMethods.weaveffi_kv_Store_get(_handle, keyPtr, out var outLen, ref err); - WeaveFFIError.CheckKv(err); - var resultBuf = new byte[(int)outLen]; - if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); - NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - Entry? value = null; - if (valueReader.ReadOptionFlag()) - { - var valueValue = Entry.ReadFrom(valueReader); - value = valueValue; - } - valueReader.ExpectEnd(); - return value; - } - finally - { - Marshal.FreeCoTaskMem(keyPtr); - } - } - - /// Remove the entry for the given key, returning true if it existed - /// Thrown when the call reports a KvError code. - public bool Delete(string key) - { - var err = new WeaveFFIError(); - var keyPtr = Marshal.StringToCoTaskMemUTF8(key); - try - { - var result = NativeMethods.weaveffi_kv_Store_delete(_handle, keyPtr, ref err); - WeaveFFIError.CheckKv(err); - return result != 0; - } - finally - { - Marshal.FreeCoTaskMem(keyPtr); - } - } - - /// Stream every key, optionally filtered by a prefix - /// Streams lazily: each element is pulled from the native - /// iterator on demand, and the iterator is destroyed when enumeration - /// completes or the enumerator is disposed (a foreach disposes it - /// automatically, including on early exit). The returned sequence can be - /// enumerated only once. - /// Thrown when the call reports a KvError code. - public IEnumerable ListKeys(string? prefix) - { - var err = new WeaveFFIError(); - var prefixWriter = new WeaveFFIBufferWriter(); - if (prefix != null) - { - prefixWriter.WriteOptionFlag(true); - prefixWriter.WriteString(prefix!); - } - else - { - prefixWriter.WriteOptionFlag(false); - } - var prefixBuf = prefixWriter.ToArray(); - var prefixPin = GCHandle.Alloc(prefixBuf, GCHandleType.Pinned); - try - { - var iter = NativeMethods.weaveffi_kv_Store_list_keys(_handle, prefixPin.AddrOfPinnedObject(), (UIntPtr)prefixBuf.Length, ref err); - WeaveFFIError.CheckKv(err); - return new WeaveFFIOnceEnumerable(EnumerateListKeys(iter)); - } - finally - { - prefixPin.Free(); - } - } - - private static IEnumerator EnumerateListKeys(IntPtr iter) - { - try - { - while (true) - { - var iterErr = new WeaveFFIError(); - if (NativeMethods.weaveffi_kv_Store_ListKeysIterator_next(iter, out var out_item, ref iterErr) == 0) - { - WeaveFFIError.CheckKv(iterErr); - yield break; - } - WeaveFFIError.CheckKv(iterErr); - var item = Marshal.PtrToStringUTF8(out_item) ?? ""; - NativeMethods.weaveffi_free_string(out_item); - yield return item; - } - } - finally - { - NativeMethods.weaveffi_kv_Store_ListKeysIterator_destroy(iter); - } - } - - /// Return the number of live entries in the store - public long Count() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kv_Store_count(_handle, ref err); - WeaveFFIError.Check(err); - return result; - } - - /// Drop every entry from the store - public void Clear() - { - var err = new WeaveFFIError(); - NativeMethods.weaveffi_kv_Store_clear(_handle, ref err); - WeaveFFIError.Check(err); - } - - /// Reclaim space asynchronously; returns the number of bytes reclaimed - /// Thrown when the call reports a KvError code. - public async Task Compact() - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - NativeMethods.AsyncCb_weaveffi_kv_Store_compact callback = (context, err, result) => - { - try - { - if (err != IntPtr.Zero) - { - var wErr = Marshal.PtrToStructure(err); - if (wErr.Code != 0) - { - var msg = Marshal.PtrToStringUTF8(wErr.Message) ?? ""; - var payload = WeaveFFIError.CopyPayload(wErr); - NativeMethods.weaveffi_error_free(err); - tcs.SetException(KvException.FromCode(wErr.Code, msg, payload)); - return; - } - } - tcs.SetResult(result); - } - finally - { - if (context != IntPtr.Zero) - { - GCHandle.FromIntPtr(context).Free(); - } - } - }; - var gcHandle = GCHandle.Alloc(callback, GCHandleType.Normal); - var ctx = GCHandle.ToIntPtr(gcHandle); - try - { - NativeMethods.weaveffi_kv_Store_compact_async(_handle, IntPtr.Zero, callback, ctx); - } - catch - { - if (gcHandle.IsAllocated) gcHandle.Free(); - throw; - } - return await tcs.Task; - } - - /// Legacy single-shot put kept for compatibility - /// Thrown when the call reports a KvError code. - [Obsolete("use put() with explicit kind")] - public bool LegacyPut(string key, byte[] value) - { - var err = new WeaveFFIError(); - var keyPtr = Marshal.StringToCoTaskMemUTF8(key); - var valuePin = GCHandle.Alloc(value, GCHandleType.Pinned); - try - { - var result = NativeMethods.weaveffi_kv_Store_legacy_put(_handle, keyPtr, valuePin.AddrOfPinnedObject(), (UIntPtr)value.Length, ref err); - WeaveFFIError.CheckKv(err); - return result != 0; - } - finally - { - Marshal.FreeCoTaskMem(keyPtr); - valuePin.Free(); - } - } - - /// The largest number of live entries one store will hold - public static long DefaultCapacity() - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kv_Store_default_capacity(ref err); - WeaveFFIError.Check(err); - return result; - } - - public void Dispose() - { - if (!_disposed) - { - NativeMethods.weaveffi_kv_Store_destroy(_handle); - _disposed = true; - } - GC.SuppressFinalize(this); - } - - ~Store() - { - Dispose(); - } - } - - /// Aggregate store statistics - public sealed class Stats - { - /// Number of live entries - public long TotalEntries { get; } - - /// Sum of all value byte lengths - public long TotalBytes { get; } - - /// Number of entries past their TTL but not yet evicted - public long ExpiredEntries { get; } - - public Stats(long totalEntries, long totalBytes, long expiredEntries) - { - TotalEntries = totalEntries; - TotalBytes = totalBytes; - ExpiredEntries = expiredEntries; - } - - internal void WriteTo(WeaveFFIBufferWriter writer) - { - writer.WriteI64(TotalEntries); - writer.WriteI64(TotalBytes); - writer.WriteI64(ExpiredEntries); - } - - internal static Stats ReadFrom(WeaveFFIBufferReader reader) - { - var fTotalEntries = reader.ReadI64(); - var fTotalBytes = reader.ReadI64(); - var fExpiredEntries = reader.ReadI64(); - return new Stats(fTotalEntries, fTotalBytes, fExpiredEntries); - } - } - - internal static class NativeMethods - { - private const string LibName = "weaveffi"; - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_free_string(IntPtr ptr); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_free_bytes(IntPtr ptr, UIntPtr len); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_clear(ref WeaveFFIError err); - - [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_error_free(IntPtr err); - - [DllImport(LibName, EntryPoint = "weaveffi_kv_Store_destroy", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_kv_Store_destroy(IntPtr self); - - [DllImport(LibName, EntryPoint = "weaveffi_kv_Store_open", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kv_Store_open(IntPtr path, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_kv_Store_put", CallingConvention = CallingConvention.Cdecl)] - internal static extern byte weaveffi_kv_Store_put(IntPtr self, IntPtr key, IntPtr value_ptr, UIntPtr value_len, int kind, IntPtr ttl_seconds_ptr, UIntPtr ttl_seconds_len, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_kv_Store_get", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kv_Store_get(IntPtr self, IntPtr key, out UIntPtr out_len, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_kv_Store_delete", CallingConvention = CallingConvention.Cdecl)] - internal static extern byte weaveffi_kv_Store_delete(IntPtr self, IntPtr key, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_kv_Store_list_keys", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kv_Store_list_keys(IntPtr self, IntPtr prefix_ptr, UIntPtr prefix_len, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kv_Store_ListKeysIterator_next", CallingConvention = CallingConvention.Cdecl)] - internal static extern int weaveffi_kv_Store_ListKeysIterator_next(IntPtr iter, out IntPtr out_item, ref WeaveFFIError out_err); - - [DllImport(LibName, EntryPoint = "weaveffi_kv_Store_ListKeysIterator_destroy", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_kv_Store_ListKeysIterator_destroy(IntPtr iter); - - [DllImport(LibName, EntryPoint = "weaveffi_kv_Store_count", CallingConvention = CallingConvention.Cdecl)] - internal static extern long weaveffi_kv_Store_count(IntPtr self, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_kv_Store_clear", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_kv_Store_clear(IntPtr self, ref WeaveFFIError err); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - internal delegate void AsyncCb_weaveffi_kv_Store_compact(IntPtr context, IntPtr err, long result); - - [DllImport(LibName, EntryPoint = "weaveffi_kv_Store_compact_async", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_kv_Store_compact_async(IntPtr self, IntPtr cancel_token, AsyncCb_weaveffi_kv_Store_compact callback, IntPtr context); - - [DllImport(LibName, EntryPoint = "weaveffi_kv_Store_legacy_put", CallingConvention = CallingConvention.Cdecl)] - internal static extern byte weaveffi_kv_Store_legacy_put(IntPtr self, IntPtr key, IntPtr value_ptr, UIntPtr value_len, ref WeaveFFIError err); - - [DllImport(LibName, EntryPoint = "weaveffi_kv_Store_default_capacity", CallingConvention = CallingConvention.Cdecl)] - internal static extern long weaveffi_kv_Store_default_capacity(ref WeaveFFIError err); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - internal delegate void Cb_weaveffi_kv_OnEvict_fn(IntPtr key, IntPtr context); - - [DllImport(LibName, EntryPoint = "weaveffi_kv_register_eviction_listener", CallingConvention = CallingConvention.Cdecl)] - internal static extern ulong weaveffi_kv_register_eviction_listener(Cb_weaveffi_kv_OnEvict_fn callback, IntPtr context); - - [DllImport(LibName, EntryPoint = "weaveffi_kv_unregister_eviction_listener", CallingConvention = CallingConvention.Cdecl)] - internal static extern void weaveffi_kv_unregister_eviction_listener(ulong id); - - [DllImport(LibName, EntryPoint = "weaveffi_kv_stats_get_stats", CallingConvention = CallingConvention.Cdecl)] - internal static extern IntPtr weaveffi_kv_stats_get_stats(IntPtr store, out UIntPtr out_len, ref WeaveFFIError err); - - } - - public static class Kv - { - private static readonly object _listenerLock = new object(); - // Live listener delegates by subscription id. Holding the delegate - // here keeps its native thunk alive until unregistered; without this - // the GC could collect a delegate the producer still calls. - private static readonly Dictionary _listenerRefs = new Dictionary(); - - /// Subscribe to per-key eviction notifications - /// A subscription id for UnregisterEvictionListener(). - public static ulong RegisterEvictionListener(Action callback) - { - NativeMethods.Cb_weaveffi_kv_OnEvict_fn trampoline = (key, context) => - { - callback(Marshal.PtrToStringUTF8(key) ?? ""); - }; - ulong id; - lock (_listenerLock) - { - id = NativeMethods.weaveffi_kv_register_eviction_listener(trampoline, IntPtr.Zero); - _listenerRefs[id] = trampoline; - } - return id; - } - - /// Unregisters a listener previously registered with RegisterEvictionListener(). - public static void UnregisterEvictionListener(ulong id) - { - NativeMethods.weaveffi_kv_unregister_eviction_listener(id); - lock (_listenerLock) - { - _listenerRefs.Remove(id); - } - } - - } - - public static class KvStats - { - /// Snapshot the current store statistics - /// Thrown when the call reports a KvError code. - public static Stats GetStats(Store store) - { - var err = new WeaveFFIError(); - var result = NativeMethods.weaveffi_kv_stats_get_stats(store.Handle, out var outLen, ref err); - WeaveFFIError.CheckKv(err); - var resultBuf = new byte[(int)outLen]; - if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); - NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var value = Stats.ReadFrom(valueReader); - valueReader.ExpectEnd(); - return value; - } - - } - -} - -// END WeaveFFI.cs diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_kvstore__WeaveFFI_csproj.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kvstore__WeaveFFI_csproj.snap deleted file mode 100644 index 97f37904..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_kvstore__WeaveFFI_csproj.snap +++ /dev/null @@ -1,20 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - - - - - net8.0 - WeaveFFI - 0.1.0 - WeaveFFI bindings generated by WeaveFFI - true - - - - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_kvstore__WeaveFFI_nuspec.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_kvstore__WeaveFFI_nuspec.snap deleted file mode 100644 index 55db5121..00000000 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_kvstore__WeaveFFI_nuspec.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - - - - - WeaveFFI - 0.1.0 - WeaveFFI Contributors - WeaveFFI bindings generated by WeaveFFI - MIT - https://github.com/weavefoundry/weaveffi - ffi interop native pinvoke - - - - diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__WeaveFFI_cs.snap index ac956587..404b351b 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__WeaveFFI_cs.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_nested_modules__WeaveFFI_cs.snap @@ -472,6 +472,34 @@ namespace WeaveFFI { private const string LibName = "weaveffi"; + // The ABI revision these bindings were generated against. + internal const uint AbiVersion = 1; + + // Runs before the first P/Invoke through this class, so a producer built + // for a different ABI revision fails loudly instead of misreading the + // error struct or a value buffer later. + static NativeMethods() + { + uint found; + try + { + found = weaveffi_abi_version(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded WeaveFFI library predates ABI versioning (these bindings expect ABI revision {AbiVersion})", e); + } + if (found != AbiVersion) + { + throw new InvalidOperationException( + $"WeaveFFI ABI mismatch: these bindings expect revision {AbiVersion} but the loaded library reports revision {found}"); + } + } + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern uint weaveffi_abi_version(); + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] internal static extern void weaveffi_free_string(IntPtr ptr); @@ -509,10 +537,10 @@ namespace WeaveFFI var resultBuf = new byte[(int)outLen]; if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var value = Canvas.ReadFrom(valueReader); - valueReader.ExpectEnd(); - return value; + var decodedReader = new WeaveFFIBufferReader(resultBuf); + var decoded = Canvas.ReadFrom(decodedReader); + decodedReader.ExpectEnd(); + return decoded; } } @@ -528,10 +556,10 @@ namespace WeaveFFI var resultBuf = new byte[(int)outLen]; if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var value = Shape.ReadFrom(valueReader); - valueReader.ExpectEnd(); - return value; + var decodedReader = new WeaveFFIBufferReader(resultBuf); + var decoded = Shape.ReadFrom(decodedReader); + decodedReader.ExpectEnd(); + return decoded; } /// Nested function returning a struct owned by an ancestor module. @@ -543,10 +571,10 @@ namespace WeaveFFI var resultBuf = new byte[(int)outLen]; if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var value = Canvas.ReadFrom(valueReader); - valueReader.ExpectEnd(); - return value; + var decodedReader = new WeaveFFIBufferReader(resultBuf); + var decoded = Canvas.ReadFrom(decodedReader); + decodedReader.ExpectEnd(); + return decoded; } } @@ -569,10 +597,10 @@ namespace WeaveFFI var resultBuf = new byte[(int)outLen]; if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var value = Detail.ReadFrom(valueReader); - valueReader.ExpectEnd(); - return value; + var decodedReader = new WeaveFFIBufferReader(resultBuf); + var decoded = Detail.ReadFrom(decodedReader); + decodedReader.ExpectEnd(); + return decoded; } finally { diff --git a/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__WeaveFFI_cs.snap b/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__WeaveFFI_cs.snap index 6563d3f7..299bdc8e 100644 --- a/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__WeaveFFI_cs.snap +++ b/crates/weaveffi-cli/tests/snapshots/dotnet_shapes__WeaveFFI_cs.snap @@ -557,6 +557,34 @@ namespace WeaveFFI { private const string LibName = "weaveffi"; + // The ABI revision these bindings were generated against. + internal const uint AbiVersion = 1; + + // Runs before the first P/Invoke through this class, so a producer built + // for a different ABI revision fails loudly instead of misreading the + // error struct or a value buffer later. + static NativeMethods() + { + uint found; + try + { + found = weaveffi_abi_version(); + } + catch (EntryPointNotFoundException e) + { + throw new InvalidOperationException( + $"the loaded WeaveFFI library predates ABI versioning (these bindings expect ABI revision {AbiVersion})", e); + } + if (found != AbiVersion) + { + throw new InvalidOperationException( + $"WeaveFFI ABI mismatch: these bindings expect revision {AbiVersion} but the loaded library reports revision {found}"); + } + } + + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] + internal static extern uint weaveffi_abi_version(); + [DllImport(LibName, CallingConvention = CallingConvention.Cdecl)] internal static extern void weaveffi_free_string(IntPtr ptr); @@ -622,10 +650,10 @@ namespace WeaveFFI var resultBuf = new byte[(int)outLen]; if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var value = Shape.ReadFrom(valueReader); - valueReader.ExpectEnd(); - return value; + var decodedReader = new WeaveFFIBufferReader(resultBuf); + var decoded = Shape.ReadFrom(decodedReader); + decodedReader.ExpectEnd(); + return decoded; } finally { @@ -659,10 +687,10 @@ namespace WeaveFFI var resultBuf = new byte[(int)outLen]; if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen); NativeMethods.weaveffi_free_bytes(result, outLen); - var valueReader = new WeaveFFIBufferReader(resultBuf); - var value = Numerics.ReadFrom(valueReader); - valueReader.ExpectEnd(); - return value; + var decodedReader = new WeaveFFIBufferReader(resultBuf); + var decoded = Numerics.ReadFrom(decodedReader); + decodedReader.ExpectEnd(); + return decoded; } } diff --git a/crates/weaveffi-cli/tests/snapshots/go_async_demo__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_async_demo__weaveffi_go.snap deleted file mode 100644 index 0b0ec545..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_async_demo__weaveffi_go.snap +++ /dev/null @@ -1,525 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -package weaveffi - -/* -#cgo LDFLAGS: -lweaveffi -#include "weaveffi.h" -#include -extern void goWv_weaveffi_tasks_run_task_callback(void* context, weaveffi_error* err, uint8_t* result_ptr, size_t result_len); -extern void goWv_weaveffi_tasks_run_batch_callback(void* context, weaveffi_error* err, uint8_t* result_ptr, size_t result_len); -extern void goWv_weaveffi_tasks_run_n_tasks_callback(void* context, weaveffi_error* err, int32_t result); -*/ -import "C" - -import ( - "encoding/binary" - "fmt" - "math" - "sync" - "unicode/utf8" - "unsafe" -) - -func boolToC(b bool) C._Bool { - return C._Bool(b) -} - -func cToBool(b C._Bool) bool { - return bool(b) -} - -// WeaveFFIError reports a failure crossing the C boundary that no typed -// error domain claims: an unknown code, a marshalling failure, or a -// producer panic. -type WeaveFFIError struct { - // Code is the numeric ABI error code. - Code int32 - // Message is the human-readable error message. - Message string -} - -func (e *WeaveFFIError) Error() string { - return fmt.Sprintf("weaveffi: %s (code %d)", e.Message, e.Code) -} - -// wvTakeError reads and clears a non-zero C error slot, returning its -// code, message, and a copy of its structured payload buffer (nil when -// the code declares no payload fields). -func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { - code := int32(cErr.code) - msg := "" - if cErr.message != nil { - msg = C.GoString(cErr.message) - } - var payload []byte - if cErr.payload_ptr != nil { - payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len)) - } - C.weaveffi_error_clear(cErr) - return code, msg, payload -} - -// wvTakeBoxedError reads a heap-boxed error delivered to an async -// completion callback and releases the box with weaveffi_error_free. -func wvTakeBoxedError(cErr *C.weaveffi_error) (int32, string, []byte) { - code := int32(cErr.code) - msg := "" - if cErr.message != nil { - msg = C.GoString(cErr.message) - } - var payload []byte - if cErr.payload_ptr != nil { - payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len)) - } - C.weaveffi_error_free(cErr) - return code, msg, payload -} - -func wvBrandError(code int32, message string, _ []byte) error { - return &WeaveFFIError{Code: code, Message: message} -} - -// wvTrap panics when the C error slot reports a failure. Non-throwing -// wrappers check their slot with it: a non-zero code there can only be -// a producer panic or a marshalling failure. -func wvTrap(cErr *C.weaveffi_error) { - if cErr.code != 0 { - code, msg, _ := wvTakeError(cErr) - panic(fmt.Sprintf("weaveffi: %s (code %d)", msg, code)) - } -} - -// wvWriter serializes values into the WeaveFFI 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) > uint64(^uint32(0)) { - panic("weaveffi: 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...) -} - -func (w *wvWriter) writeOptionFlag(present bool) { - w.writeBool(present) -} - -// wvReader decodes values from the WeaveFFI value-buffer format. A -// malformed buffer is a producer/consumer contract violation, so every -// read panics (the same channel a trapped producer error uses) instead -// of returning a typed domain error. -type wvReader struct { - buf []byte - pos int -} - -func wvMalformed(context string) { - panic("weaveffi: 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()) -} - -func (r *wvReader) readLen() int { - n := int(r.readU32()) - if n > len(r.buf)-r.pos { - wvMalformed("length prefix exceeds remaining buffer") - } - return n -} - -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 { - b := r.take(r.readLen(), "byte buffer") - out := make([]byte, len(b)) - copy(out, b) - return out -} - -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") - } -} - -// wvCopyBuffer copies an owned, producer-allocated value buffer into Go -// memory and releases it with weaveffi_free_bytes. -func wvCopyBuffer(ptr *C.uint8_t, length C.size_t) []byte { - if ptr == nil { - return nil - } - out := C.GoBytes(unsafe.Pointer(ptr), C.int(length)) - C.weaveffi_free_bytes(ptr, length) - return out -} - -// wvBorrowBuffer copies a borrowed value buffer into Go memory. The -// producer keeps ownership and frees it after the borrowing call returns. -func wvBorrowBuffer(ptr *C.uint8_t, length C.size_t) []byte { - if ptr == nil { - return nil - } - return C.GoBytes(unsafe.Pointer(ptr), C.int(length)) -} - -var ( - wvCallbackMu sync.Mutex - wvCallbackSeq uint64 - wvCallbacks = map[uint64]interface{}{} -) - -func wvCallbackStore(v interface{}) uint64 { - wvCallbackMu.Lock() - defer wvCallbackMu.Unlock() - wvCallbackSeq++ - wvCallbacks[wvCallbackSeq] = v - return wvCallbackSeq -} - -func wvCallbackLoad(id uint64) interface{} { - wvCallbackMu.Lock() - defer wvCallbackMu.Unlock() - return wvCallbacks[id] -} - -func wvCallbackTake(id uint64) interface{} { - wvCallbackMu.Lock() - defer wvCallbackMu.Unlock() - v := wvCallbacks[id] - delete(wvCallbacks, id) - return v -} - -func wvCallbackDelete(id uint64) { - wvCallbackMu.Lock() - defer wvCallbackMu.Unlock() - delete(wvCallbacks, id) -} - -// TaskError is a typed error reported by the `tasks` module. -type TaskError struct { - // Code is the numeric ABI error code (one of the TaskError constants). - Code int32 - // Message is the human-readable error message. - Message string -} - -func (e *TaskError) Error() string { - return fmt.Sprintf("tasks: %s (code %d)", e.Message, e.Code) -} - -// TaskError codes. -const ( - // TaskErrorInvalidName task name must not be empty - TaskErrorInvalidName int32 = 1 -) - -// wvMapTasks converts a non-zero code from the `tasks` domain into a -// *TaskError, falling back to the generic *WeaveFFIError for unknown codes. -func wvMapTasks(code int32, message string, payload []byte) error { - switch code { - case TaskErrorInvalidName: - if message == "" { - message = "task name must not be empty" - } - return &TaskError{Code: code, Message: message} - default: - return wvBrandError(code, message, payload) - } -} - -type TaskResult struct { - Id int64 - Value string - Success bool -} - -// wvPackTaskResult appends v to w in the value-buffer wire format. -func wvPackTaskResult(w *wvWriter, v TaskResult) { - w.writeI64(v.Id) - w.writeString(v.Value) - w.writeBool(v.Success) -} - -// wvUnpackTaskResult decodes one TaskResult from r. -func wvUnpackTaskResult(r *wvReader) TaskResult { - var v TaskResult - v.Id = r.readI64() - v.Value = r.readString() - v.Success = r.readBool() - return v -} - -type wvOutcomeTasksRunTask struct { - val TaskResult - err error -} - -//export goWv_weaveffi_tasks_run_task_callback -func goWv_weaveffi_tasks_run_task_callback(context unsafe.Pointer, err *C.weaveffi_error, result_ptr *C.uint8_t, result_len C.size_t) { - v := wvCallbackTake(uint64(uintptr(context))) - if v == nil { - return - } - ch := v.(chan wvOutcomeTasksRunTask) - if err != nil && err.code != 0 { - ch <- wvOutcomeTasksRunTask{err: wvMapTasks(wvTakeBoxedError(err))} - return - } - rRes := &wvReader{buf: wvCopyBuffer(result_ptr, result_len)} - var val TaskResult - val = wvUnpackTaskResult(rRes) - rRes.expectEnd() - ch <- wvOutcomeTasksRunTask{val: val} -} - -// Blocks the calling goroutine until the async producer completes. -func RunTask(name string) (TaskResult, error) { - ch := make(chan wvOutcomeTasksRunTask, 1) - ctxID := wvCallbackStore(ch) - cName := C.CString(name) - defer C.free(unsafe.Pointer(cName)) - C.weaveffi_tasks_run_task_async(cName, C.weaveffi_tasks_run_task_callback(unsafe.Pointer(C.goWv_weaveffi_tasks_run_task_callback)), unsafe.Pointer(uintptr(ctxID))) - outcome := <-ch - if outcome.err != nil { - return TaskResult{}, outcome.err - } - return outcome.val, nil -} - -type wvOutcomeTasksRunBatch struct { - val []TaskResult - err error -} - -//export goWv_weaveffi_tasks_run_batch_callback -func goWv_weaveffi_tasks_run_batch_callback(context unsafe.Pointer, err *C.weaveffi_error, result_ptr *C.uint8_t, result_len C.size_t) { - v := wvCallbackTake(uint64(uintptr(context))) - if v == nil { - return - } - ch := v.(chan wvOutcomeTasksRunBatch) - if err != nil && err.code != 0 { - ch <- wvOutcomeTasksRunBatch{err: wvBrandError(wvTakeBoxedError(err))} - return - } - rRes := &wvReader{buf: wvCopyBuffer(result_ptr, result_len)} - var val []TaskResult - nRes0 := rRes.readLen() - val = make([]TaskResult, nRes0) - for iRes0 := range val { - val[iRes0] = wvUnpackTaskResult(rRes) - } - rRes.expectEnd() - ch <- wvOutcomeTasksRunBatch{val: val} -} - -// Blocks the calling goroutine until the async producer completes. -func RunBatch(names []string) []TaskResult { - ch := make(chan wvOutcomeTasksRunBatch, 1) - ctxID := wvCallbackStore(ch) - wNames := &wvWriter{} - wNames.writeLen(len(names)) - for _, eNames0 := range names { - wNames.writeString(eNames0) - } - var cNamesPtr *C.uint8_t - if len(wNames.buf) > 0 { - cNamesPtr = (*C.uint8_t)(unsafe.Pointer(&wNames.buf[0])) - } - C.weaveffi_tasks_run_batch_async(cNamesPtr, C.size_t(len(wNames.buf)), C.weaveffi_tasks_run_batch_callback(unsafe.Pointer(C.goWv_weaveffi_tasks_run_batch_callback)), unsafe.Pointer(uintptr(ctxID))) - outcome := <-ch - if outcome.err != nil { - panic(outcome.err) - } - return outcome.val -} - -func CancelTask(id int64) bool { - var cErr C.weaveffi_error - result := C.weaveffi_tasks_cancel_task(C.int64_t(id), &cErr) - wvTrap(&cErr) - return cToBool(result) -} - -type wvOutcomeTasksRunNTasks struct { - val int32 - err error -} - -//export goWv_weaveffi_tasks_run_n_tasks_callback -func goWv_weaveffi_tasks_run_n_tasks_callback(context unsafe.Pointer, err *C.weaveffi_error, result C.int32_t) { - v := wvCallbackTake(uint64(uintptr(context))) - if v == nil { - return - } - ch := v.(chan wvOutcomeTasksRunNTasks) - if err != nil && err.code != 0 { - ch <- wvOutcomeTasksRunNTasks{err: wvBrandError(wvTakeBoxedError(err))} - return - } - ch <- wvOutcomeTasksRunNTasks{val: int32(result)} -} - -// Blocks the calling goroutine until the async producer completes. -func RunNTasks(n int32) int32 { - ch := make(chan wvOutcomeTasksRunNTasks, 1) - ctxID := wvCallbackStore(ch) - C.weaveffi_tasks_run_n_tasks_async(C.int32_t(n), C.weaveffi_tasks_run_n_tasks_callback(unsafe.Pointer(C.goWv_weaveffi_tasks_run_n_tasks_callback)), unsafe.Pointer(uintptr(ctxID))) - outcome := <-ch - if outcome.err != nil { - panic(outcome.err) - } - return outcome.val -} - -func ActiveCallbacks() int64 { - var cErr C.weaveffi_error - result := C.weaveffi_tasks_active_callbacks(&cErr) - wvTrap(&cErr) - return int64(result) -} - - -// END weaveffi.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_calculator__README_md.snap b/crates/weaveffi-cli/tests/snapshots/go_calculator__README_md.snap deleted file mode 100644 index 2bc2b438..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_calculator__README_md.snap +++ /dev/null @@ -1,46 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI Go Bindings - -Auto-generated Go bindings using CGo. - -## Prerequisites - -- Go >= 1.23 (the bindings return standard `iter` package sequences) -- A C compiler (gcc or clang) accessible to CGo -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, - or `weaveffi.dll`) and the C header (`weaveffi.h`) - -## Build - -1. Place `libweaveffi.so` (or the platform-specific equivalent) and - `weaveffi.h` where the linker and CGo can find them. For example, - install them into `/usr/local/lib` and `/usr/local/include`, or set - `CGO_LDFLAGS` and `CGO_CFLAGS`: - -```sh -export CGO_CFLAGS="-I/path/to/headers" -export CGO_LDFLAGS="-L/path/to/lib -lweaveffi" -``` - -2. Build or run your Go project that imports this module: - -```sh -go build ./... -``` - -## How It Works - -The generated `weaveffi.go` file uses a CGo preamble to `#include "weaveffi.h"` -and link against `-lweaveffi`. Each API function is exposed as an idiomatic Go -function that marshals arguments to C types, calls the C ABI function, and -converts the result back to Go types. Records, rich enums, optionals, lists, -and maps cross the boundary serialized in the WeaveFFI value-buffer format. -Errors are returned as Go `error` values. - - diff --git a/crates/weaveffi-cli/tests/snapshots/go_calculator__go_mod.snap b/crates/weaveffi-cli/tests/snapshots/go_calculator__go_mod.snap deleted file mode 100644 index 73a087a7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_calculator__go_mod.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -module weaveffi - -go 1.23 - -// END go.mod diff --git a/crates/weaveffi-cli/tests/snapshots/go_calculator__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_calculator__weaveffi_go.snap deleted file mode 100644 index 074a1b1b..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_calculator__weaveffi_go.snap +++ /dev/null @@ -1,154 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -package weaveffi - -/* -#cgo LDFLAGS: -lweaveffi -#include "weaveffi.h" -#include -*/ -import "C" - -import ( - "fmt" - "unsafe" -) - -// WeaveFFIError reports a failure crossing the C boundary that no typed -// error domain claims: an unknown code, a marshalling failure, or a -// producer panic. -type WeaveFFIError struct { - // Code is the numeric ABI error code. - Code int32 - // Message is the human-readable error message. - Message string -} - -func (e *WeaveFFIError) Error() string { - return fmt.Sprintf("weaveffi: %s (code %d)", e.Message, e.Code) -} - -// wvTakeError reads and clears a non-zero C error slot, returning its -// code, message, and a copy of its structured payload buffer (nil when -// the code declares no payload fields). -func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { - code := int32(cErr.code) - msg := "" - if cErr.message != nil { - msg = C.GoString(cErr.message) - } - var payload []byte - if cErr.payload_ptr != nil { - payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len)) - } - C.weaveffi_error_clear(cErr) - return code, msg, payload -} - -// wvTakeBoxedError reads a heap-boxed error delivered to an async -// completion callback and releases the box with weaveffi_error_free. -func wvTakeBoxedError(cErr *C.weaveffi_error) (int32, string, []byte) { - code := int32(cErr.code) - msg := "" - if cErr.message != nil { - msg = C.GoString(cErr.message) - } - var payload []byte - if cErr.payload_ptr != nil { - payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len)) - } - C.weaveffi_error_free(cErr) - return code, msg, payload -} - -func wvBrandError(code int32, message string, _ []byte) error { - return &WeaveFFIError{Code: code, Message: message} -} - -// wvTrap panics when the C error slot reports a failure. Non-throwing -// wrappers check their slot with it: a non-zero code there can only be -// a producer panic or a marshalling failure. -func wvTrap(cErr *C.weaveffi_error) { - if cErr.code != 0 { - code, msg, _ := wvTakeError(cErr) - panic(fmt.Sprintf("weaveffi: %s (code %d)", msg, code)) - } -} - -// CalcError is a typed error reported by the `calculator` module. -type CalcError struct { - // Code is the numeric ABI error code (one of the CalcError constants). - Code int32 - // Message is the human-readable error message. - Message string -} - -func (e *CalcError) Error() string { - return fmt.Sprintf("calculator: %s (code %d)", e.Message, e.Code) -} - -// CalcError codes. -const ( - // CalcErrorDivisionByZero division by zero - CalcErrorDivisionByZero int32 = 1 -) - -// wvMapCalculator converts a non-zero code from the `calculator` domain into a -// *CalcError, falling back to the generic *WeaveFFIError for unknown codes. -func wvMapCalculator(code int32, message string, payload []byte) error { - switch code { - case CalcErrorDivisionByZero: - if message == "" { - message = "division by zero" - } - return &CalcError{Code: code, Message: message} - default: - return wvBrandError(code, message, payload) - } -} - -// Add: Add two integers -func Add(a int32, b int32) int32 { - var cErr C.weaveffi_error - result := C.weaveffi_calculator_add(C.int32_t(a), C.int32_t(b), &cErr) - wvTrap(&cErr) - return int32(result) -} - -// Mul: Multiply two integers -func Mul(a int32, b int32) int32 { - var cErr C.weaveffi_error - result := C.weaveffi_calculator_mul(C.int32_t(a), C.int32_t(b), &cErr) - wvTrap(&cErr) - return int32(result) -} - -// Div: Divide two integers, failing on a zero divisor -func Div(a int32, b int32) (int32, error) { - var cErr C.weaveffi_error - result := C.weaveffi_calculator_div(C.int32_t(a), C.int32_t(b), &cErr) - if cErr.code != 0 { - return 0, wvMapCalculator(wvTakeError(&cErr)) - } - return int32(result), nil -} - -// Echo: Echo a string back -func Echo(s string) string { - cS := C.CString(s) - defer C.free(unsafe.Pointer(cS)) - var cErr C.weaveffi_error - result := C.weaveffi_calculator_echo(cS, &cErr) - wvTrap(&cErr) - goResult := C.GoString(result) - C.weaveffi_free_string(result) - return goResult -} - - -// END weaveffi.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_contacts__README_md.snap b/crates/weaveffi-cli/tests/snapshots/go_contacts__README_md.snap deleted file mode 100644 index 2bc2b438..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_contacts__README_md.snap +++ /dev/null @@ -1,46 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI Go Bindings - -Auto-generated Go bindings using CGo. - -## Prerequisites - -- Go >= 1.23 (the bindings return standard `iter` package sequences) -- A C compiler (gcc or clang) accessible to CGo -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, - or `weaveffi.dll`) and the C header (`weaveffi.h`) - -## Build - -1. Place `libweaveffi.so` (or the platform-specific equivalent) and - `weaveffi.h` where the linker and CGo can find them. For example, - install them into `/usr/local/lib` and `/usr/local/include`, or set - `CGO_LDFLAGS` and `CGO_CFLAGS`: - -```sh -export CGO_CFLAGS="-I/path/to/headers" -export CGO_LDFLAGS="-L/path/to/lib -lweaveffi" -``` - -2. Build or run your Go project that imports this module: - -```sh -go build ./... -``` - -## How It Works - -The generated `weaveffi.go` file uses a CGo preamble to `#include "weaveffi.h"` -and link against `-lweaveffi`. Each API function is exposed as an idiomatic Go -function that marshals arguments to C types, calls the C ABI function, and -converts the result back to Go types. Records, rich enums, optionals, lists, -and maps cross the boundary serialized in the WeaveFFI value-buffer format. -Errors are returned as Go `error` values. - - diff --git a/crates/weaveffi-cli/tests/snapshots/go_contacts__go_mod.snap b/crates/weaveffi-cli/tests/snapshots/go_contacts__go_mod.snap deleted file mode 100644 index 73a087a7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_contacts__go_mod.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -module weaveffi - -go 1.23 - -// END go.mod diff --git a/crates/weaveffi-cli/tests/snapshots/go_contacts__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_contacts__weaveffi_go.snap deleted file mode 100644 index c209027b..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_contacts__weaveffi_go.snap +++ /dev/null @@ -1,479 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -package weaveffi - -/* -#cgo LDFLAGS: -lweaveffi -#include "weaveffi.h" -#include -*/ -import "C" - -import ( - "encoding/binary" - "fmt" - "math" - "unicode/utf8" - "unsafe" -) - -func boolToC(b bool) C._Bool { - return C._Bool(b) -} - -func cToBool(b C._Bool) bool { - return bool(b) -} - -// WeaveFFIError reports a failure crossing the C boundary that no typed -// error domain claims: an unknown code, a marshalling failure, or a -// producer panic. -type WeaveFFIError struct { - // Code is the numeric ABI error code. - Code int32 - // Message is the human-readable error message. - Message string -} - -func (e *WeaveFFIError) Error() string { - return fmt.Sprintf("weaveffi: %s (code %d)", e.Message, e.Code) -} - -// wvTakeError reads and clears a non-zero C error slot, returning its -// code, message, and a copy of its structured payload buffer (nil when -// the code declares no payload fields). -func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { - code := int32(cErr.code) - msg := "" - if cErr.message != nil { - msg = C.GoString(cErr.message) - } - var payload []byte - if cErr.payload_ptr != nil { - payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len)) - } - C.weaveffi_error_clear(cErr) - return code, msg, payload -} - -// wvTakeBoxedError reads a heap-boxed error delivered to an async -// completion callback and releases the box with weaveffi_error_free. -func wvTakeBoxedError(cErr *C.weaveffi_error) (int32, string, []byte) { - code := int32(cErr.code) - msg := "" - if cErr.message != nil { - msg = C.GoString(cErr.message) - } - var payload []byte - if cErr.payload_ptr != nil { - payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len)) - } - C.weaveffi_error_free(cErr) - return code, msg, payload -} - -func wvBrandError(code int32, message string, _ []byte) error { - return &WeaveFFIError{Code: code, Message: message} -} - -// wvTrap panics when the C error slot reports a failure. Non-throwing -// wrappers check their slot with it: a non-zero code there can only be -// a producer panic or a marshalling failure. -func wvTrap(cErr *C.weaveffi_error) { - if cErr.code != 0 { - code, msg, _ := wvTakeError(cErr) - panic(fmt.Sprintf("weaveffi: %s (code %d)", msg, code)) - } -} - -// wvWriter serializes values into the WeaveFFI 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) > uint64(^uint32(0)) { - panic("weaveffi: 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...) -} - -func (w *wvWriter) writeOptionFlag(present bool) { - w.writeBool(present) -} - -// wvReader decodes values from the WeaveFFI value-buffer format. A -// malformed buffer is a producer/consumer contract violation, so every -// read panics (the same channel a trapped producer error uses) instead -// of returning a typed domain error. -type wvReader struct { - buf []byte - pos int -} - -func wvMalformed(context string) { - panic("weaveffi: 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()) -} - -func (r *wvReader) readLen() int { - n := int(r.readU32()) - if n > len(r.buf)-r.pos { - wvMalformed("length prefix exceeds remaining buffer") - } - return n -} - -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 { - b := r.take(r.readLen(), "byte buffer") - out := make([]byte, len(b)) - copy(out, b) - return out -} - -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") - } -} - -// wvCopyBuffer copies an owned, producer-allocated value buffer into Go -// memory and releases it with weaveffi_free_bytes. -func wvCopyBuffer(ptr *C.uint8_t, length C.size_t) []byte { - if ptr == nil { - return nil - } - out := C.GoBytes(unsafe.Pointer(ptr), C.int(length)) - C.weaveffi_free_bytes(ptr, length) - return out -} - -// wvBorrowBuffer copies a borrowed value buffer into Go memory. The -// producer keeps ownership and frees it after the borrowing call returns. -func wvBorrowBuffer(ptr *C.uint8_t, length C.size_t) []byte { - if ptr == nil { - return nil - } - return C.GoBytes(unsafe.Pointer(ptr), C.int(length)) -} - -// ContactsError is a typed error reported by the `contacts` module. -type ContactsError struct { - // Code is the numeric ABI error code (one of the ContactsError constants). - Code int32 - // Message is the human-readable error message. - Message string -} - -func (e *ContactsError) Error() string { - return fmt.Sprintf("contacts: %s (code %d)", e.Message, e.Code) -} - -// ContactsError codes. -const ( - // ContactsErrorInvalidName name must not be empty - ContactsErrorInvalidName int32 = 1 - // ContactsErrorNotFound contact not found - ContactsErrorNotFound int32 = 2 -) - -// wvMapContacts converts a non-zero code from the `contacts` domain into a -// *ContactsError, falling back to the generic *WeaveFFIError for unknown codes. -func wvMapContacts(code int32, message string, payload []byte) error { - switch code { - case ContactsErrorInvalidName: - if message == "" { - message = "name must not be empty" - } - return &ContactsError{Code: code, Message: message} - case ContactsErrorNotFound: - if message == "" { - message = "contact not found" - } - return &ContactsError{Code: code, Message: message} - default: - return wvBrandError(code, message, payload) - } -} - -type ContactType int32 - -const ( - ContactTypePersonal ContactType = 0 - ContactTypeWork ContactType = 1 - ContactTypeOther ContactType = 2 -) - -type Contact struct { - Id int64 - FirstName string - LastName string - Email *string - ContactType ContactType -} - -// wvPackContact appends v to w in the value-buffer wire format. -func wvPackContact(w *wvWriter, v Contact) { - w.writeI64(v.Id) - w.writeString(v.FirstName) - w.writeString(v.LastName) - if v.Email == nil { - w.writeOptionFlag(false) - } else { - w.writeOptionFlag(true) - w.writeString((*v.Email)) - } - w.writeI32(int32(v.ContactType)) -} - -// wvUnpackContact decodes one Contact from r. -func wvUnpackContact(r *wvReader) Contact { - var v Contact - v.Id = r.readI64() - v.FirstName = r.readString() - v.LastName = r.readString() - if r.readOptionFlag() { - var oEmail0 string - oEmail0 = r.readString() - v.Email = &oEmail0 - } - v.ContactType = ContactType(r.readI32()) - return v -} - -// ContactBook: An in-memory address book owning its contacts -type ContactBook struct { - ptr *C.weaveffi_contacts_ContactBook -} - -func NewContactBook() *ContactBook { - var cErr C.weaveffi_error - result := C.weaveffi_contacts_ContactBook_new(&cErr) - wvTrap(&cErr) - return &ContactBook{ptr: result} -} - -func (s *ContactBook) Add(firstName string, lastName string, email *string, contactType ContactType) (Contact, error) { - cFirstName := C.CString(firstName) - defer C.free(unsafe.Pointer(cFirstName)) - cLastName := C.CString(lastName) - defer C.free(unsafe.Pointer(cLastName)) - wEmail := &wvWriter{} - if email == nil { - wEmail.writeOptionFlag(false) - } else { - wEmail.writeOptionFlag(true) - wEmail.writeString((*email)) - } - var cEmailPtr *C.uint8_t - if len(wEmail.buf) > 0 { - cEmailPtr = (*C.uint8_t)(unsafe.Pointer(&wEmail.buf[0])) - } - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_contacts_ContactBook_add(s.ptr, cFirstName, cLastName, cEmailPtr, C.size_t(len(wEmail.buf)), C.weaveffi_contacts_ContactType(contactType), &cOutLen, &cErr) - if cErr.code != 0 { - return Contact{}, wvMapContacts(wvTakeError(&cErr)) - } - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult Contact - goResult = wvUnpackContact(rRes) - rRes.expectEnd() - return goResult, nil -} - -func (s *ContactBook) Get(id int64) (Contact, error) { - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_contacts_ContactBook_get(s.ptr, C.int64_t(id), &cOutLen, &cErr) - if cErr.code != 0 { - return Contact{}, wvMapContacts(wvTakeError(&cErr)) - } - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult Contact - goResult = wvUnpackContact(rRes) - rRes.expectEnd() - return goResult, nil -} - -func (s *ContactBook) List() []Contact { - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_contacts_ContactBook_list(s.ptr, &cOutLen, &cErr) - wvTrap(&cErr) - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult []Contact - nRes0 := rRes.readLen() - goResult = make([]Contact, nRes0) - for iRes0 := range goResult { - goResult[iRes0] = wvUnpackContact(rRes) - } - rRes.expectEnd() - return goResult -} - -func (s *ContactBook) Remove(id int64) bool { - var cErr C.weaveffi_error - result := C.weaveffi_contacts_ContactBook_remove(s.ptr, C.int64_t(id), &cErr) - wvTrap(&cErr) - return cToBool(result) -} - -func (s *ContactBook) Count() int32 { - var cErr C.weaveffi_error - result := C.weaveffi_contacts_ContactBook_count(s.ptr, &cErr) - wvTrap(&cErr) - return int32(result) -} - -func (s *ContactBook) Close() { - if s.ptr != nil { - C.weaveffi_contacts_ContactBook_destroy(s.ptr) - s.ptr = nil - } -} - - -// END weaveffi.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__weaveffi_go.snap index 0e8c0acd..ded4cc7d 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__weaveffi_go.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_docs_everywhere__weaveffi_go.snap @@ -25,6 +25,15 @@ import ( "unsafe" ) +// The ABI revision these bindings were generated against. +const wvABIVersion uint32 = 1 + +func init() { + if found := uint32(C.weaveffi_abi_version()); found != wvABIVersion { + panic(fmt.Sprintf("WeaveFFI ABI mismatch: these bindings expect revision %d but the loaded library reports revision %d", wvABIVersion, found)) + } +} + func boolToC(b bool) C._Bool { return C._Bool(b) } diff --git a/crates/weaveffi-cli/tests/snapshots/go_async_demo__README_md.snap b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__README_md.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/go_async_demo__README_md.snap rename to crates/weaveffi-cli/tests/snapshots/go_edge_cases__README_md.snap diff --git a/crates/weaveffi-cli/tests/snapshots/go_async_demo__go_mod.snap b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__go_mod.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/go_async_demo__go_mod.snap rename to crates/weaveffi-cli/tests/snapshots/go_edge_cases__go_mod.snap diff --git a/crates/weaveffi-cli/tests/snapshots/go_edge_cases__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__weaveffi_go.snap new file mode 100644 index 00000000..84829bad --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/go_edge_cases__weaveffi_go.snap @@ -0,0 +1,1609 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from weaveffi.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate weaveffi.yml -o + +package weaveffi + +/* +#cgo LDFLAGS: -lweaveffi +#include "weaveffi.h" +#include +extern void goWv_weaveffi_edge_cases_OnBatch_fn(uint8_t* items_ptr, size_t items_len, uint8_t* raw_ptr, size_t raw_len, uint8_t* first_ptr, size_t first_len, weaveffi_edge_cases_Mode mode, void* context); +extern void goWv_weaveffi_edge_cases_OnTick_fn(void* context); +extern void goWv_weaveffi_edge_cases_async_void_callback(void* context, weaveffi_error* err); +extern void goWv_weaveffi_edge_cases_async_bytes_callback(void* context, weaveffi_error* err, uint8_t* result, size_t result_len); +extern void goWv_weaveffi_edge_cases_async_struct_callback(void* context, weaveffi_error* err, uint8_t* result_ptr, size_t result_len); +extern void goWv_weaveffi_edge_cases_async_list_callback(void* context, weaveffi_error* err, uint8_t* result_ptr, size_t result_len); +extern void goWv_weaveffi_edge_cases_async_optional_callback(void* context, weaveffi_error* err, uint8_t* result_ptr, size_t result_len); +extern void goWv_weaveffi_edge_cases_async_enum_callback(void* context, weaveffi_error* err, weaveffi_edge_cases_Mode result); +extern void goWv_weaveffi_edge_cases_async_keywords_callback(void* context, weaveffi_error* err, char* result); +extern void goWv_weaveffi_edge_cases_Widget_fetch_callback(void* context, weaveffi_error* err, uint8_t* result_ptr, size_t result_len); +*/ +import "C" + +import ( + "encoding/binary" + "fmt" + "iter" + "math" + "sync" + "unicode/utf8" + "unsafe" +) + +// The ABI revision these bindings were generated against. +const wvABIVersion uint32 = 1 + +func init() { + if found := uint32(C.weaveffi_abi_version()); found != wvABIVersion { + panic(fmt.Sprintf("WeaveFFI ABI mismatch: these bindings expect revision %d but the loaded library reports revision %d", wvABIVersion, found)) + } +} + +func boolToC(b bool) C._Bool { + return C._Bool(b) +} + +func cToBool(b C._Bool) bool { + return bool(b) +} + +// WeaveFFIError reports a failure crossing the C boundary that no typed +// error domain claims: an unknown code, a marshalling failure, or a +// producer panic. +type WeaveFFIError struct { + // Code is the numeric ABI error code. + Code int32 + // Message is the human-readable error message. + Message string +} + +func (e *WeaveFFIError) Error() string { + return fmt.Sprintf("weaveffi: %s (code %d)", e.Message, e.Code) +} + +// wvTakeError reads and clears a non-zero C error slot, returning its +// code, message, and a copy of its structured payload buffer (nil when +// the code declares no payload fields). +func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { + code := int32(cErr.code) + msg := "" + if cErr.message != nil { + msg = C.GoString(cErr.message) + } + var payload []byte + if cErr.payload_ptr != nil { + payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len)) + } + C.weaveffi_error_clear(cErr) + return code, msg, payload +} + +// wvTakeBoxedError reads a heap-boxed error delivered to an async +// completion callback and releases the box with weaveffi_error_free. +func wvTakeBoxedError(cErr *C.weaveffi_error) (int32, string, []byte) { + code := int32(cErr.code) + msg := "" + if cErr.message != nil { + msg = C.GoString(cErr.message) + } + var payload []byte + if cErr.payload_ptr != nil { + payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len)) + } + C.weaveffi_error_free(cErr) + return code, msg, payload +} + +func wvBrandError(code int32, message string, _ []byte) error { + return &WeaveFFIError{Code: code, Message: message} +} + +// wvTrap panics when the C error slot reports a failure. Non-throwing +// wrappers check their slot with it: a non-zero code there can only be +// a producer panic or a marshalling failure. +func wvTrap(cErr *C.weaveffi_error) { + if cErr.code != 0 { + code, msg, _ := wvTakeError(cErr) + panic(fmt.Sprintf("weaveffi: %s (code %d)", msg, code)) + } +} + +// wvWriter serializes values into the WeaveFFI 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) > uint64(^uint32(0)) { + panic("weaveffi: 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...) +} + +func (w *wvWriter) writeOptionFlag(present bool) { + w.writeBool(present) +} + +// wvReader decodes values from the WeaveFFI value-buffer format. A +// malformed buffer is a producer/consumer contract violation, so every +// read panics (the same channel a trapped producer error uses) instead +// of returning a typed domain error. +type wvReader struct { + buf []byte + pos int +} + +func wvMalformed(context string) { + panic("weaveffi: 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()) +} + +func (r *wvReader) readLen() int { + n := int(r.readU32()) + if n > len(r.buf)-r.pos { + wvMalformed("length prefix exceeds remaining buffer") + } + return n +} + +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 { + b := r.take(r.readLen(), "byte buffer") + out := make([]byte, len(b)) + copy(out, b) + return out +} + +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") + } +} + +// wvCopyBuffer copies an owned, producer-allocated value buffer into Go +// memory and releases it with weaveffi_free_bytes. +func wvCopyBuffer(ptr *C.uint8_t, length C.size_t) []byte { + if ptr == nil { + return nil + } + out := C.GoBytes(unsafe.Pointer(ptr), C.int(length)) + C.weaveffi_free_bytes(ptr, length) + return out +} + +// wvBorrowBuffer copies a borrowed value buffer into Go memory. The +// producer keeps ownership and frees it after the borrowing call returns. +func wvBorrowBuffer(ptr *C.uint8_t, length C.size_t) []byte { + if ptr == nil { + return nil + } + return C.GoBytes(unsafe.Pointer(ptr), C.int(length)) +} + +var ( + wvCallbackMu sync.Mutex + wvCallbackSeq uint64 + wvCallbacks = map[uint64]interface{}{} + // Subscription id -> registry id, so unregister can release the Go callback. + wvListenerCtx = map[uint64]uint64{} +) + +func wvCallbackStore(v interface{}) uint64 { + wvCallbackMu.Lock() + defer wvCallbackMu.Unlock() + wvCallbackSeq++ + wvCallbacks[wvCallbackSeq] = v + return wvCallbackSeq +} + +func wvCallbackLoad(id uint64) interface{} { + wvCallbackMu.Lock() + defer wvCallbackMu.Unlock() + return wvCallbacks[id] +} + +func wvCallbackTake(id uint64) interface{} { + wvCallbackMu.Lock() + defer wvCallbackMu.Unlock() + v := wvCallbacks[id] + delete(wvCallbacks, id) + return v +} + +func wvCallbackDelete(id uint64) { + wvCallbackMu.Lock() + defer wvCallbackMu.Unlock() + delete(wvCallbacks, id) +} + +// LeafHandle is a typed handle naming a producer-owned resource. It wraps +// the opaque C pointer and owes no release call. +type LeafHandle struct { + ptr *C.weaveffi_edge_cases_Leaf +} + +// EdgeErrorsError is a typed error reported by the `edge_cases` module. +type EdgeErrorsError struct { + // Code is the numeric ABI error code (one of the EdgeErrorsError constants). + Code int32 + // Message is the human-readable error message. + Message string +} + +func (e *EdgeErrorsError) Error() string { + return fmt.Sprintf("edge_cases: %s (code %d)", e.Message, e.Code) +} + +// EdgeErrorsError codes. +const ( + // EdgeErrorsErrorBusy: Resource is busy + EdgeErrorsErrorBusy int32 = 7 +) + +// wvMapEdgeCases converts a non-zero code from the `edge_cases` domain into a +// *EdgeErrorsError, falling back to the generic *WeaveFFIError for unknown codes. +func wvMapEdgeCases(code int32, message string, payload []byte) error { + switch code { + case EdgeErrorsErrorBusy: + if message == "" { + message = "Resource is busy" + } + return &EdgeErrorsError{Code: code, Message: message} + default: + return wvBrandError(code, message, payload) + } +} + +// Legacy: Deprecated enum with non-contiguous values and keyword-like variants +type Legacy int32 + +const ( + LegacyNone Legacy = 0 + LegacyDefault Legacy = 5 + LegacyClass Legacy = 10 +) + +type Mode int32 + +const ( + ModeFast Mode = 1 + ModeSafe Mode = 2 +) + +type Leaf struct { + Weight float32 +} + +// wvPackLeaf appends v to w in the value-buffer wire format. +func wvPackLeaf(w *wvWriter, v Leaf) { + w.writeF32(v.Weight) +} + +// wvUnpackLeaf decodes one Leaf from r. +func wvUnpackLeaf(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 +} + +// wvPackInner appends v to w in the value-buffer wire format. +func wvPackInner(w *wvWriter, v Inner) { + wvPackLeaf(w, v.Leaf) + w.writeLen(len(v.Leaves)) + for _, eLeaves0 := range v.Leaves { + wvPackLeaf(w, eLeaves0) + } +} + +// wvUnpackInner decodes one Inner from r. +func wvUnpackInner(r *wvReader) Inner { + var v Inner + v.Leaf = wvUnpackLeaf(r) + nLeaves0 := r.readLen() + v.Leaves = make([]Leaf, nLeaves0) + for iLeaves0 := range v.Leaves { + v.Leaves[iLeaves0] = wvUnpackLeaf(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 +} + +// wvPackOuter appends v to w in the value-buffer wire format. +func wvPackOuter(w *wvWriter, v Outer) { + wvPackInner(w, v.Inner) + if v.MaybeInner == nil { + w.writeOptionFlag(false) + } else { + w.writeOptionFlag(true) + wvPackInner(w, (*v.MaybeInner)) + } + w.writeLen(len(v.ByName)) + for kByName0, vByName0 := range v.ByName { + w.writeString(kByName0) + wvPackInner(w, vByName0) + } + w.writeLen(len(v.Modes)) + for _, eModes0 := range v.Modes { + w.writeI32(int32(eModes0)) + } + w.writeLen(len(v.Matrix)) + for _, eMatrix0 := range v.Matrix { + w.writeLen(len(eMatrix0)) + for _, eMatrix1 := range eMatrix0 { + w.writeI32(eMatrix1) + } + } + w.writeLen(len(v.Sparse)) + for _, eSparse0 := range v.Sparse { + if eSparse0 == nil { + w.writeOptionFlag(false) + } else { + w.writeOptionFlag(true) + w.writeI64((*eSparse0)) + } + } + if v.Blob == nil { + w.writeOptionFlag(false) + } else { + w.writeOptionFlag(true) + w.writeBytes(v.Blob) + } + if v.Note == nil { + w.writeOptionFlag(false) + } else { + w.writeOptionFlag(true) + w.writeString((*v.Note)) + } + w.writeLen(len(v.ById)) + for kById0, vById0 := range v.ById { + w.writeI64(kById0) + w.writeString(vById0) + } + w.writeBool(v.Hidden) + w.writeU8(v.V2) +} + +// wvUnpackOuter decodes one Outer from r. +func wvUnpackOuter(r *wvReader) Outer { + var v Outer + v.Inner = wvUnpackInner(r) + if r.readOptionFlag() { + var oMaybeInner0 Inner + oMaybeInner0 = wvUnpackInner(r) + v.MaybeInner = &oMaybeInner0 + } + nByName0 := r.readLen() + v.ByName = make(map[string]Inner, nByName0) + for iByName0 := 0; iByName0 < nByName0; iByName0++ { + var kByName0 string + kByName0 = r.readString() + var vByName0 Inner + vByName0 = wvUnpackInner(r) + v.ByName[kByName0] = vByName0 + } + nModes0 := r.readLen() + v.Modes = make([]Mode, nModes0) + for iModes0 := range v.Modes { + v.Modes[iModes0] = Mode(r.readI32()) + } + nMatrix0 := r.readLen() + v.Matrix = make([][]int32, nMatrix0) + for iMatrix0 := range v.Matrix { + nMatrix1 := r.readLen() + v.Matrix[iMatrix0] = make([]int32, nMatrix1) + for iMatrix1 := range v.Matrix[iMatrix0] { + v.Matrix[iMatrix0][iMatrix1] = r.readI32() + } + } + nSparse0 := r.readLen() + v.Sparse = make([]*int64, nSparse0) + for iSparse0 := range v.Sparse { + if r.readOptionFlag() { + var oSparse1 int64 + oSparse1 = r.readI64() + v.Sparse[iSparse0] = &oSparse1 + } + } + if r.readOptionFlag() { + var oBlob0 []byte + oBlob0 = r.readBytes() + v.Blob = oBlob0 + } + if r.readOptionFlag() { + var oNote0 string + oNote0 = r.readString() + v.Note = &oNote0 + } + nById0 := r.readLen() + v.ById = make(map[int64]string, nById0) + for iById0 := 0; iById0 < nById0; iById0++ { + var kById0 int64 + kById0 = r.readI64() + var vById0 string + vById0 = r.readString() + v.ById[kById0] = vById0 + } + 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 +} + +// wvPackKeywords appends v to w in the value-buffer wire format. +func wvPackKeywords(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) +} + +// wvUnpackKeywords decodes one Keywords from r. +func wvUnpackKeywords(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 +type Retired struct { + Id int64 +} + +// wvPackRetired appends v to w in the value-buffer wire format. +func wvPackRetired(w *wvWriter, v Retired) { + w.writeI64(v.Id) +} + +// wvUnpackRetired decodes one Retired from r. +func wvUnpackRetired(r *wvReader) Retired { + var v Retired + v.Id = r.readI64() + return v +} + +// Widget: Interface with no constructor; instances come from make_widget +type Widget struct { + ptr *C.weaveffi_edge_cases_Widget +} + +// Refresh: Void method that can fail +func (s *Widget) Refresh() error { + var cErr C.weaveffi_error + C.weaveffi_edge_cases_Widget_refresh(s.ptr, &cErr) + if cErr.code != 0 { + return wvMapEdgeCases(wvTakeError(&cErr)) + } + return nil +} + +func (s *Widget) Rename(class string, import_ *string) string { + cClass := C.CString(class) + defer C.free(unsafe.Pointer(cClass)) + wImport := &wvWriter{} + if import_ == nil { + wImport.writeOptionFlag(false) + } else { + wImport.writeOptionFlag(true) + wImport.writeString((*import_)) + } + var cImportPtr *C.uint8_t + if len(wImport.buf) > 0 { + cImportPtr = (*C.uint8_t)(unsafe.Pointer(&wImport.buf[0])) + } + var cErr C.weaveffi_error + result := C.weaveffi_edge_cases_Widget_rename(s.ptr, cClass, cImportPtr, C.size_t(len(wImport.buf)), &cErr) + wvTrap(&cErr) + goResult := C.GoString(result) + C.weaveffi_free_string(result) + return goResult +} + +type wvOutcomeEdgeCasesWidgetFetch struct { + val Outer + err error +} + +//export goWv_weaveffi_edge_cases_Widget_fetch_callback +func goWv_weaveffi_edge_cases_Widget_fetch_callback(context unsafe.Pointer, err *C.weaveffi_error, result_ptr *C.uint8_t, result_len C.size_t) { + v := wvCallbackTake(uint64(uintptr(context))) + if v == nil { + return + } + ch := v.(chan wvOutcomeEdgeCasesWidgetFetch) + if err != nil && err.code != 0 { + ch <- wvOutcomeEdgeCasesWidgetFetch{err: wvMapEdgeCases(wvTakeBoxedError(err))} + return + } + rRes := &wvReader{buf: wvCopyBuffer(result_ptr, result_len)} + var val Outer + val = wvUnpackOuter(rRes) + rRes.expectEnd() + ch <- wvOutcomeEdgeCasesWidgetFetch{val: val} +} + +// Fetch: Async method with a struct result +// Blocks the calling goroutine until the async producer completes. +func (s *Widget) Fetch(id int64) (Outer, error) { + ch := make(chan wvOutcomeEdgeCasesWidgetFetch, 1) + ctxID := wvCallbackStore(ch) + C.weaveffi_edge_cases_Widget_fetch_async(s.ptr, C.int64_t(id), C.weaveffi_edge_cases_Widget_fetch_callback(unsafe.Pointer(C.goWv_weaveffi_edge_cases_Widget_fetch_callback)), unsafe.Pointer(uintptr(ctxID))) + outcome := <-ch + if outcome.err != nil { + return Outer{}, outcome.err + } + return outcome.val, nil +} + +// Deprecated: Use rename instead +func (s *Widget) OldName() string { + var cErr C.weaveffi_error + result := C.weaveffi_edge_cases_Widget_old_name(s.ptr, &cErr) + wvTrap(&cErr) + goResult := C.GoString(result) + C.weaveffi_free_string(result) + return goResult +} + +func (s *Widget) Close() { + if s.ptr != nil { + C.weaveffi_edge_cases_Widget_destroy(s.ptr) + s.ptr = nil + } +} + +// Vault: Deprecated interface with a throwing constructor +type Vault struct { + ptr *C.weaveffi_edge_cases_Vault +} + +// OpenVault: Fails when the vault is busy +func OpenVault(path string) (*Vault, error) { + cPath := C.CString(path) + defer C.free(unsafe.Pointer(cPath)) + var cErr C.weaveffi_error + result := C.weaveffi_edge_cases_Vault_open(cPath, &cErr) + if cErr.code != 0 { + return nil, wvMapEdgeCases(wvTakeError(&cErr)) + } + return &Vault{ptr: result}, nil +} + +func (s *Vault) Peek() *Widget { + var cErr C.weaveffi_error + result := C.weaveffi_edge_cases_Vault_peek(s.ptr, &cErr) + wvTrap(&cErr) + if result == nil { + return nil + } + return &Widget{ptr: result} +} + +// VaultProbe: Throwing static +func VaultProbe() (bool, error) { + var cErr C.weaveffi_error + result := C.weaveffi_edge_cases_Vault_probe(&cErr) + if cErr.code != 0 { + return false, wvMapEdgeCases(wvTakeError(&cErr)) + } + return cToBool(result), nil +} + +func (s *Vault) Close() { + if s.ptr != nil { + C.weaveffi_edge_cases_Vault_destroy(s.ptr) + s.ptr = nil + } +} + +//export goWv_weaveffi_edge_cases_OnBatch_fn +func goWv_weaveffi_edge_cases_OnBatch_fn(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.weaveffi_edge_cases_Mode, context unsafe.Pointer) { + v := wvCallbackLoad(uint64(uintptr(context))) + if v == nil { + return + } + cb := v.(func(items []Outer, raw []byte, first *Outer, mode Mode)) + rArg0 := &wvReader{buf: wvBorrowBuffer(items_ptr, items_len)} + var arg0 []Outer + nArg00 := rArg0.readLen() + arg0 = make([]Outer, nArg00) + for iArg00 := range arg0 { + arg0[iArg00] = wvUnpackOuter(rArg0) + } + rArg0.expectEnd() + var arg1 []byte + if raw_ptr != nil { + arg1 = C.GoBytes(unsafe.Pointer(raw_ptr), C.int(raw_len)) + } + rArg2 := &wvReader{buf: wvBorrowBuffer(first_ptr, first_len)} + var arg2 *Outer + if rArg2.readOptionFlag() { + var oArg20 Outer + oArg20 = wvUnpackOuter(rArg2) + arg2 = &oArg20 + } + rArg2.expectEnd() + arg3 := Mode(mode) + cb(arg0, arg1, arg2, arg3) +} + +//export goWv_weaveffi_edge_cases_OnTick_fn +func goWv_weaveffi_edge_cases_OnTick_fn(context unsafe.Pointer) { + v := wvCallbackLoad(uint64(uintptr(context))) + if v == nil { + return + } + cb := v.(func()) + cb() +} + +// Returns a subscription id for UnregisterBatchListener. +func RegisterBatchListener(callback func(items []Outer, raw []byte, first *Outer, mode Mode)) uint64 { + ctxID := wvCallbackStore(callback) + id := uint64(C.weaveffi_edge_cases_register_batch_listener(C.weaveffi_edge_cases_OnBatch_fn(unsafe.Pointer(C.goWv_weaveffi_edge_cases_OnBatch_fn)), unsafe.Pointer(uintptr(ctxID)))) + wvCallbackMu.Lock() + wvListenerCtx[id] = ctxID + wvCallbackMu.Unlock() + return id +} + +// UnregisterBatchListener unregisters a listener previously registered with RegisterBatchListener. +func UnregisterBatchListener(id uint64) { + C.weaveffi_edge_cases_unregister_batch_listener(C.uint64_t(id)) + wvCallbackMu.Lock() + ctxID, ok := wvListenerCtx[id] + delete(wvListenerCtx, id) + wvCallbackMu.Unlock() + if ok { + wvCallbackDelete(ctxID) + } +} + +// Returns a subscription id for UnregisterTickListener. +func RegisterTickListener(callback func()) uint64 { + ctxID := wvCallbackStore(callback) + id := uint64(C.weaveffi_edge_cases_register_tick_listener(C.weaveffi_edge_cases_OnTick_fn(unsafe.Pointer(C.goWv_weaveffi_edge_cases_OnTick_fn)), unsafe.Pointer(uintptr(ctxID)))) + wvCallbackMu.Lock() + wvListenerCtx[id] = ctxID + wvCallbackMu.Unlock() + return id +} + +// UnregisterTickListener unregisters a listener previously registered with RegisterTickListener. +func UnregisterTickListener(id uint64) { + C.weaveffi_edge_cases_unregister_tick_listener(C.uint64_t(id)) + wvCallbackMu.Lock() + ctxID, ok := wvListenerCtx[id] + delete(wvListenerCtx, id) + wvCallbackMu.Unlock() + if ok { + wvCallbackDelete(ctxID) + } +} + +// Noop: No parameters and no result +func Noop() { + var cErr C.weaveffi_error + C.weaveffi_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 { + cFrom := C.CString(from) + defer C.free(unsafe.Pointer(cFrom)) + wFunc := &wvWriter{} + wFunc.writeLen(len(func_)) + for _, eFunc0 := range func_ { + wFunc.writeString(eFunc0) + } + var cFuncPtr *C.uint8_t + if len(wFunc.buf) > 0 { + cFuncPtr = (*C.uint8_t)(unsafe.Pointer(&wFunc.buf[0])) + } + wRange := &wvWriter{} + if range_ == nil { + wRange.writeOptionFlag(false) + } else { + wRange.writeOptionFlag(true) + wRange.writeI32((*range_)) + } + var cRangePtr *C.uint8_t + if len(wRange.buf) > 0 { + cRangePtr = (*C.uint8_t)(unsafe.Pointer(&wRange.buf[0])) + } + var cErr C.weaveffi_error + result := C.weaveffi_edge_cases_select(C.int32_t(class), cFrom, cFuncPtr, C.size_t(len(wFunc.buf)), cRangePtr, C.size_t(len(wRange.buf)), boolToC(type_), &cErr) + wvTrap(&cErr) + return int32(result) +} + +// NestedOptional: Optional list of optional structs, the deepest legal nesting +func NestedOptional(items []*Outer) []*Outer { + wItems := &wvWriter{} + if items == nil { + wItems.writeOptionFlag(false) + } else { + wItems.writeOptionFlag(true) + wItems.writeLen(len(items)) + for _, eItems1 := range items { + if eItems1 == nil { + wItems.writeOptionFlag(false) + } else { + wItems.writeOptionFlag(true) + wvPackOuter(wItems, (*eItems1)) + } + } + } + var cItemsPtr *C.uint8_t + if len(wItems.buf) > 0 { + cItemsPtr = (*C.uint8_t)(unsafe.Pointer(&wItems.buf[0])) + } + var cOutLen C.size_t + var cErr C.weaveffi_error + result := C.weaveffi_edge_cases_nested_optional(cItemsPtr, C.size_t(len(wItems.buf)), &cOutLen, &cErr) + wvTrap(&cErr) + rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} + var goResult []*Outer + if rRes.readOptionFlag() { + var oRes0 []*Outer + nRes1 := rRes.readLen() + oRes0 = make([]*Outer, nRes1) + for iRes1 := range oRes0 { + if rRes.readOptionFlag() { + var oRes2 Outer + oRes2 = wvUnpackOuter(rRes) + oRes0[iRes1] = &oRes2 + } + } + goResult = oRes0 + } + rRes.expectEnd() + return goResult +} + +func OptionalParams(text *string, count *int32, outer *Outer, tags []string, blob []byte, mode *Mode) *Mode { + wText := &wvWriter{} + if text == nil { + wText.writeOptionFlag(false) + } else { + wText.writeOptionFlag(true) + wText.writeString((*text)) + } + var cTextPtr *C.uint8_t + if len(wText.buf) > 0 { + cTextPtr = (*C.uint8_t)(unsafe.Pointer(&wText.buf[0])) + } + wCount := &wvWriter{} + if count == nil { + wCount.writeOptionFlag(false) + } else { + wCount.writeOptionFlag(true) + wCount.writeI32((*count)) + } + var cCountPtr *C.uint8_t + if len(wCount.buf) > 0 { + cCountPtr = (*C.uint8_t)(unsafe.Pointer(&wCount.buf[0])) + } + wOuter := &wvWriter{} + if outer == nil { + wOuter.writeOptionFlag(false) + } else { + wOuter.writeOptionFlag(true) + wvPackOuter(wOuter, (*outer)) + } + var cOuterPtr *C.uint8_t + if len(wOuter.buf) > 0 { + cOuterPtr = (*C.uint8_t)(unsafe.Pointer(&wOuter.buf[0])) + } + wTags := &wvWriter{} + if tags == nil { + wTags.writeOptionFlag(false) + } else { + wTags.writeOptionFlag(true) + wTags.writeLen(len(tags)) + for _, eTags1 := range tags { + wTags.writeString(eTags1) + } + } + var cTagsPtr *C.uint8_t + if len(wTags.buf) > 0 { + cTagsPtr = (*C.uint8_t)(unsafe.Pointer(&wTags.buf[0])) + } + wBlob := &wvWriter{} + if blob == nil { + wBlob.writeOptionFlag(false) + } else { + wBlob.writeOptionFlag(true) + wBlob.writeBytes(blob) + } + var cBlobPtr *C.uint8_t + if len(wBlob.buf) > 0 { + cBlobPtr = (*C.uint8_t)(unsafe.Pointer(&wBlob.buf[0])) + } + wMode := &wvWriter{} + if mode == nil { + wMode.writeOptionFlag(false) + } else { + wMode.writeOptionFlag(true) + wMode.writeI32(int32((*mode))) + } + var cModePtr *C.uint8_t + if len(wMode.buf) > 0 { + cModePtr = (*C.uint8_t)(unsafe.Pointer(&wMode.buf[0])) + } + var cOutLen C.size_t + var cErr C.weaveffi_error + result := C.weaveffi_edge_cases_optional_params(cTextPtr, C.size_t(len(wText.buf)), cCountPtr, C.size_t(len(wCount.buf)), cOuterPtr, C.size_t(len(wOuter.buf)), cTagsPtr, C.size_t(len(wTags.buf)), cBlobPtr, C.size_t(len(wBlob.buf)), cModePtr, C.size_t(len(wMode.buf)), &cOutLen, &cErr) + wvTrap(&cErr) + rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} + var goResult *Mode + if rRes.readOptionFlag() { + var oRes0 Mode + oRes0 = Mode(rRes.readI32()) + goResult = &oRes0 + } + rRes.expectEnd() + return goResult +} + +// MutableParams: Write-back parameters +func MutableParams(text string, blob []byte) { + cText := C.CString(text) + defer C.free(unsafe.Pointer(cText)) + var cBlobPtr *C.uint8_t + cBlobLen := C.size_t(len(blob)) + if len(blob) > 0 { + cBlobPtr = (*C.uint8_t)(unsafe.Pointer(&blob[0])) + } + var cErr C.weaveffi_error + C.weaveffi_edge_cases_mutable_params(cText, cBlobPtr, cBlobLen, &cErr) + wvTrap(&cErr) +} + +// MakeWidget: Factory for an interface with no constructor +func MakeWidget(label string) *Widget { + cLabel := C.CString(label) + defer C.free(unsafe.Pointer(cLabel)) + var cErr C.weaveffi_error + result := C.weaveffi_edge_cases_make_widget(cLabel, &cErr) + wvTrap(&cErr) + return &Widget{ptr: result} +} + +// FindWidget: Optional interface return +func FindWidget(id int64) (*Widget, error) { + var cErr C.weaveffi_error + result := C.weaveffi_edge_cases_find_widget(C.int64_t(id), &cErr) + if cErr.code != 0 { + return nil, wvMapEdgeCases(wvTakeError(&cErr)) + } + if result == nil { + return nil, nil + } + return &Widget{ptr: result}, nil +} + +// InspectWidget: Interface parameter, including an optional one +func InspectWidget(widget *Widget, fallback *Widget) string { + var cFallback *C.weaveffi_edge_cases_Widget + if fallback != nil { + cFallback = fallback.ptr + } + var cErr C.weaveffi_error + result := C.weaveffi_edge_cases_inspect_widget(widget.ptr, cFallback, &cErr) + wvTrap(&cErr) + goResult := C.GoString(result) + C.weaveffi_free_string(result) + return goResult +} + +func TypedHandleParam(token *LeafHandle, raw int64) *LeafHandle { + var cErr C.weaveffi_error + result := C.weaveffi_edge_cases_typed_handle_param(token.ptr, C.weaveffi_handle_t(raw), &cErr) + wvTrap(&cErr) + return &LeafHandle{ptr: result} +} + +// StringStream: Iterator of strings +// Returns a lazy sequence: the producer iterator is launched on first +// iteration and one producer next call runs per element. The iterator is +// destroyed exactly once, whether the sequence is exhausted or abandoned +// early; each range over the sequence launches a fresh producer iterator. +// A reported error can only be a producer bug and panics with the +// weaveffi message. +func StringStream() iter.Seq[string] { + return func(yield func(string) bool) { + var cErr C.weaveffi_error + it := C.weaveffi_edge_cases_string_stream(&cErr) + wvTrap(&cErr) + defer C.weaveffi_edge_cases_StringStreamIterator_destroy(it) + for { + var outItem *C.char + var iterErr C.weaveffi_error + ok := C.weaveffi_edge_cases_StringStreamIterator_next(it, &outItem, &iterErr) != 0 + wvTrap(&iterErr) + if !ok { + return + } + item := C.GoString(outItem) + C.weaveffi_free_string(outItem) + if !yield(item) { + return + } + } + } +} + +// ScalarStream: Iterator of scalars +// Returns a lazy sequence: the producer iterator is launched on first +// iteration and one producer next call runs per element. The iterator is +// destroyed exactly once, whether the sequence is exhausted or abandoned +// early; each range over the sequence launches a fresh producer iterator. +// A reported error can only be a producer bug and panics with the +// weaveffi message. +func ScalarStream() iter.Seq[int32] { + return func(yield func(int32) bool) { + var cErr C.weaveffi_error + it := C.weaveffi_edge_cases_scalar_stream(&cErr) + wvTrap(&cErr) + defer C.weaveffi_edge_cases_ScalarStreamIterator_destroy(it) + for { + var outItem C.int32_t + var iterErr C.weaveffi_error + ok := C.weaveffi_edge_cases_ScalarStreamIterator_next(it, &outItem, &iterErr) != 0 + wvTrap(&iterErr) + if !ok { + return + } + item := int32(outItem) + if !yield(item) { + return + } + } + } +} + +// ModeStream: Iterator of enums +// Returns a lazy sequence: the producer iterator is launched on first +// iteration and one producer next call runs per element. The iterator is +// destroyed exactly once, whether the sequence is exhausted or abandoned +// early; each range over the sequence launches a fresh producer iterator. +// A reported error can only be a producer bug and panics with the +// weaveffi message. +func ModeStream() iter.Seq[Mode] { + return func(yield func(Mode) bool) { + var cErr C.weaveffi_error + it := C.weaveffi_edge_cases_mode_stream(&cErr) + wvTrap(&cErr) + defer C.weaveffi_edge_cases_ModeStreamIterator_destroy(it) + for { + var outItem C.weaveffi_edge_cases_Mode + var iterErr C.weaveffi_error + ok := C.weaveffi_edge_cases_ModeStreamIterator_next(it, &outItem, &iterErr) != 0 + wvTrap(&iterErr) + if !ok { + return + } + item := Mode(outItem) + if !yield(item) { + return + } + } + } +} + +type wvOutcomeEdgeCasesAsyncVoid struct { + err error +} + +//export goWv_weaveffi_edge_cases_async_void_callback +func goWv_weaveffi_edge_cases_async_void_callback(context unsafe.Pointer, err *C.weaveffi_error) { + v := wvCallbackTake(uint64(uintptr(context))) + if v == nil { + return + } + ch := v.(chan wvOutcomeEdgeCasesAsyncVoid) + if err != nil && err.code != 0 { + ch <- wvOutcomeEdgeCasesAsyncVoid{err: wvBrandError(wvTakeBoxedError(err))} + return + } + ch <- wvOutcomeEdgeCasesAsyncVoid{} +} + +// AsyncVoid: Async with no result +// Blocks the calling goroutine until the async producer completes. +func AsyncVoid() { + ch := make(chan wvOutcomeEdgeCasesAsyncVoid, 1) + ctxID := wvCallbackStore(ch) + C.weaveffi_edge_cases_async_void_async(C.weaveffi_edge_cases_async_void_callback(unsafe.Pointer(C.goWv_weaveffi_edge_cases_async_void_callback)), unsafe.Pointer(uintptr(ctxID))) + outcome := <-ch + if outcome.err != nil { + panic(outcome.err) + } +} + +type wvOutcomeEdgeCasesAsyncBytes struct { + val []byte + err error +} + +//export goWv_weaveffi_edge_cases_async_bytes_callback +func goWv_weaveffi_edge_cases_async_bytes_callback(context unsafe.Pointer, err *C.weaveffi_error, result *C.uint8_t, result_len C.size_t) { + v := wvCallbackTake(uint64(uintptr(context))) + if v == nil { + return + } + ch := v.(chan wvOutcomeEdgeCasesAsyncBytes) + if err != nil && err.code != 0 { + ch <- wvOutcomeEdgeCasesAsyncBytes{err: wvBrandError(wvTakeBoxedError(err))} + return + } + var val []byte + if result != nil { + val = C.GoBytes(unsafe.Pointer(result), C.int(result_len)) + C.weaveffi_free_bytes(result, result_len) + } + ch <- wvOutcomeEdgeCasesAsyncBytes{val: val} +} + +// Blocks the calling goroutine until the async producer completes. +func AsyncBytes(size uint32) []byte { + ch := make(chan wvOutcomeEdgeCasesAsyncBytes, 1) + ctxID := wvCallbackStore(ch) + C.weaveffi_edge_cases_async_bytes_async(C.uint32_t(size), C.weaveffi_edge_cases_async_bytes_callback(unsafe.Pointer(C.goWv_weaveffi_edge_cases_async_bytes_callback)), unsafe.Pointer(uintptr(ctxID))) + outcome := <-ch + if outcome.err != nil { + panic(outcome.err) + } + return outcome.val +} + +type wvOutcomeEdgeCasesAsyncStruct struct { + val Outer + err error +} + +//export goWv_weaveffi_edge_cases_async_struct_callback +func goWv_weaveffi_edge_cases_async_struct_callback(context unsafe.Pointer, err *C.weaveffi_error, result_ptr *C.uint8_t, result_len C.size_t) { + v := wvCallbackTake(uint64(uintptr(context))) + if v == nil { + return + } + ch := v.(chan wvOutcomeEdgeCasesAsyncStruct) + if err != nil && err.code != 0 { + ch <- wvOutcomeEdgeCasesAsyncStruct{err: wvMapEdgeCases(wvTakeBoxedError(err))} + return + } + rRes := &wvReader{buf: wvCopyBuffer(result_ptr, result_len)} + var val Outer + val = wvUnpackOuter(rRes) + rRes.expectEnd() + ch <- wvOutcomeEdgeCasesAsyncStruct{val: val} +} + +// Blocks the calling goroutine until the async producer completes. +func AsyncStruct() (Outer, error) { + ch := make(chan wvOutcomeEdgeCasesAsyncStruct, 1) + ctxID := wvCallbackStore(ch) + C.weaveffi_edge_cases_async_struct_async(C.weaveffi_edge_cases_async_struct_callback(unsafe.Pointer(C.goWv_weaveffi_edge_cases_async_struct_callback)), unsafe.Pointer(uintptr(ctxID))) + outcome := <-ch + if outcome.err != nil { + return Outer{}, outcome.err + } + return outcome.val, nil +} + +type wvOutcomeEdgeCasesAsyncList struct { + val []Outer + err error +} + +//export goWv_weaveffi_edge_cases_async_list_callback +func goWv_weaveffi_edge_cases_async_list_callback(context unsafe.Pointer, err *C.weaveffi_error, result_ptr *C.uint8_t, result_len C.size_t) { + v := wvCallbackTake(uint64(uintptr(context))) + if v == nil { + return + } + ch := v.(chan wvOutcomeEdgeCasesAsyncList) + if err != nil && err.code != 0 { + ch <- wvOutcomeEdgeCasesAsyncList{err: wvMapEdgeCases(wvTakeBoxedError(err))} + return + } + rRes := &wvReader{buf: wvCopyBuffer(result_ptr, result_len)} + var val []Outer + nRes0 := rRes.readLen() + val = make([]Outer, nRes0) + for iRes0 := range val { + val[iRes0] = wvUnpackOuter(rRes) + } + rRes.expectEnd() + ch <- wvOutcomeEdgeCasesAsyncList{val: val} +} + +// Blocks the calling goroutine until the async producer completes. +func AsyncList() ([]Outer, error) { + ch := make(chan wvOutcomeEdgeCasesAsyncList, 1) + ctxID := wvCallbackStore(ch) + C.weaveffi_edge_cases_async_list_async(nil, C.weaveffi_edge_cases_async_list_callback(unsafe.Pointer(C.goWv_weaveffi_edge_cases_async_list_callback)), unsafe.Pointer(uintptr(ctxID))) + outcome := <-ch + if outcome.err != nil { + return nil, outcome.err + } + return outcome.val, nil +} + +type wvOutcomeEdgeCasesAsyncOptional struct { + val *int64 + err error +} + +//export goWv_weaveffi_edge_cases_async_optional_callback +func goWv_weaveffi_edge_cases_async_optional_callback(context unsafe.Pointer, err *C.weaveffi_error, result_ptr *C.uint8_t, result_len C.size_t) { + v := wvCallbackTake(uint64(uintptr(context))) + if v == nil { + return + } + ch := v.(chan wvOutcomeEdgeCasesAsyncOptional) + if err != nil && err.code != 0 { + ch <- wvOutcomeEdgeCasesAsyncOptional{err: wvBrandError(wvTakeBoxedError(err))} + return + } + rRes := &wvReader{buf: wvCopyBuffer(result_ptr, result_len)} + var val *int64 + if rRes.readOptionFlag() { + var oRes0 int64 + oRes0 = rRes.readI64() + val = &oRes0 + } + rRes.expectEnd() + ch <- wvOutcomeEdgeCasesAsyncOptional{val: val} +} + +// Blocks the calling goroutine until the async producer completes. +func AsyncOptional() *int64 { + ch := make(chan wvOutcomeEdgeCasesAsyncOptional, 1) + ctxID := wvCallbackStore(ch) + C.weaveffi_edge_cases_async_optional_async(C.weaveffi_edge_cases_async_optional_callback(unsafe.Pointer(C.goWv_weaveffi_edge_cases_async_optional_callback)), unsafe.Pointer(uintptr(ctxID))) + outcome := <-ch + if outcome.err != nil { + panic(outcome.err) + } + return outcome.val +} + +type wvOutcomeEdgeCasesAsyncEnum struct { + val Mode + err error +} + +//export goWv_weaveffi_edge_cases_async_enum_callback +func goWv_weaveffi_edge_cases_async_enum_callback(context unsafe.Pointer, err *C.weaveffi_error, result C.weaveffi_edge_cases_Mode) { + v := wvCallbackTake(uint64(uintptr(context))) + if v == nil { + return + } + ch := v.(chan wvOutcomeEdgeCasesAsyncEnum) + if err != nil && err.code != 0 { + ch <- wvOutcomeEdgeCasesAsyncEnum{err: wvBrandError(wvTakeBoxedError(err))} + return + } + ch <- wvOutcomeEdgeCasesAsyncEnum{val: Mode(result)} +} + +// Blocks the calling goroutine until the async producer completes. +func AsyncEnum() Mode { + ch := make(chan wvOutcomeEdgeCasesAsyncEnum, 1) + ctxID := wvCallbackStore(ch) + C.weaveffi_edge_cases_async_enum_async(C.weaveffi_edge_cases_async_enum_callback(unsafe.Pointer(C.goWv_weaveffi_edge_cases_async_enum_callback)), unsafe.Pointer(uintptr(ctxID))) + outcome := <-ch + if outcome.err != nil { + panic(outcome.err) + } + return outcome.val +} + +type wvOutcomeEdgeCasesAsyncKeywords struct { + val string + err error +} + +//export goWv_weaveffi_edge_cases_async_keywords_callback +func goWv_weaveffi_edge_cases_async_keywords_callback(context unsafe.Pointer, err *C.weaveffi_error, result *C.char) { + v := wvCallbackTake(uint64(uintptr(context))) + if v == nil { + return + } + ch := v.(chan wvOutcomeEdgeCasesAsyncKeywords) + if err != nil && err.code != 0 { + ch <- wvOutcomeEdgeCasesAsyncKeywords{err: wvBrandError(wvTakeBoxedError(err))} + return + } + val := "" + if result != nil { + val = C.GoString(result) + C.weaveffi_free_string(result) + } + ch <- wvOutcomeEdgeCasesAsyncKeywords{val: val} +} + +// Blocks the calling goroutine until the async producer completes. +func AsyncKeywords(class string, import_ int32) string { + ch := make(chan wvOutcomeEdgeCasesAsyncKeywords, 1) + ctxID := wvCallbackStore(ch) + cClass := C.CString(class) + defer C.free(unsafe.Pointer(cClass)) + C.weaveffi_edge_cases_async_keywords_async(cClass, C.int32_t(import_), C.weaveffi_edge_cases_async_keywords_callback(unsafe.Pointer(C.goWv_weaveffi_edge_cases_async_keywords_callback)), unsafe.Pointer(uintptr(ctxID))) + outcome := <-ch + if outcome.err != nil { + panic(outcome.err) + } + return outcome.val +} + +func KeywordsRoundtrip(value Keywords) Keywords { + wValue := &wvWriter{} + wvPackKeywords(wValue, value) + var cValuePtr *C.uint8_t + if len(wValue.buf) > 0 { + cValuePtr = (*C.uint8_t)(unsafe.Pointer(&wValue.buf[0])) + } + var cOutLen C.size_t + var cErr C.weaveffi_error + result := C.weaveffi_edge_cases_keywords_roundtrip(cValuePtr, C.size_t(len(wValue.buf)), &cOutLen, &cErr) + wvTrap(&cErr) + rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} + var goResult Keywords + goResult = wvUnpackKeywords(rRes) + rRes.expectEnd() + return goResult +} + +// Deprecated: Use Mode-based APIs +func LegacyValue(value Legacy) Retired { + var cOutLen C.size_t + var cErr C.weaveffi_error + result := C.weaveffi_edge_cases_legacy_value(C.weaveffi_edge_cases_Legacy(value), &cOutLen, &cErr) + wvTrap(&cErr) + rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} + var goResult Retired + goResult = wvUnpackRetired(rRes) + rRes.expectEnd() + return goResult +} + + +// END weaveffi.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_events__README_md.snap b/crates/weaveffi-cli/tests/snapshots/go_events__README_md.snap deleted file mode 100644 index 2bc2b438..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_events__README_md.snap +++ /dev/null @@ -1,46 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI Go Bindings - -Auto-generated Go bindings using CGo. - -## Prerequisites - -- Go >= 1.23 (the bindings return standard `iter` package sequences) -- A C compiler (gcc or clang) accessible to CGo -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, - or `weaveffi.dll`) and the C header (`weaveffi.h`) - -## Build - -1. Place `libweaveffi.so` (or the platform-specific equivalent) and - `weaveffi.h` where the linker and CGo can find them. For example, - install them into `/usr/local/lib` and `/usr/local/include`, or set - `CGO_LDFLAGS` and `CGO_CFLAGS`: - -```sh -export CGO_CFLAGS="-I/path/to/headers" -export CGO_LDFLAGS="-L/path/to/lib -lweaveffi" -``` - -2. Build or run your Go project that imports this module: - -```sh -go build ./... -``` - -## How It Works - -The generated `weaveffi.go` file uses a CGo preamble to `#include "weaveffi.h"` -and link against `-lweaveffi`. Each API function is exposed as an idiomatic Go -function that marshals arguments to C types, calls the C ABI function, and -converts the result back to Go types. Records, rich enums, optionals, lists, -and maps cross the boundary serialized in the WeaveFFI value-buffer format. -Errors are returned as Go `error` values. - - diff --git a/crates/weaveffi-cli/tests/snapshots/go_events__go_mod.snap b/crates/weaveffi-cli/tests/snapshots/go_events__go_mod.snap deleted file mode 100644 index 73a087a7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_events__go_mod.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -module weaveffi - -go 1.23 - -// END go.mod diff --git a/crates/weaveffi-cli/tests/snapshots/go_events__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_events__weaveffi_go.snap deleted file mode 100644 index ac887889..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_events__weaveffi_go.snap +++ /dev/null @@ -1,198 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -package weaveffi - -/* -#cgo LDFLAGS: -lweaveffi -#include "weaveffi.h" -#include -extern void goWv_weaveffi_events_OnMessage_fn(char* message, void* context); -*/ -import "C" - -import ( - "fmt" - "iter" - "sync" - "unsafe" -) - -// WeaveFFIError reports a failure crossing the C boundary that no typed -// error domain claims: an unknown code, a marshalling failure, or a -// producer panic. -type WeaveFFIError struct { - // Code is the numeric ABI error code. - Code int32 - // Message is the human-readable error message. - Message string -} - -func (e *WeaveFFIError) Error() string { - return fmt.Sprintf("weaveffi: %s (code %d)", e.Message, e.Code) -} - -// wvTakeError reads and clears a non-zero C error slot, returning its -// code, message, and a copy of its structured payload buffer (nil when -// the code declares no payload fields). -func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { - code := int32(cErr.code) - msg := "" - if cErr.message != nil { - msg = C.GoString(cErr.message) - } - var payload []byte - if cErr.payload_ptr != nil { - payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len)) - } - C.weaveffi_error_clear(cErr) - return code, msg, payload -} - -// wvTakeBoxedError reads a heap-boxed error delivered to an async -// completion callback and releases the box with weaveffi_error_free. -func wvTakeBoxedError(cErr *C.weaveffi_error) (int32, string, []byte) { - code := int32(cErr.code) - msg := "" - if cErr.message != nil { - msg = C.GoString(cErr.message) - } - var payload []byte - if cErr.payload_ptr != nil { - payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len)) - } - C.weaveffi_error_free(cErr) - return code, msg, payload -} - -func wvBrandError(code int32, message string, _ []byte) error { - return &WeaveFFIError{Code: code, Message: message} -} - -// wvTrap panics when the C error slot reports a failure. Non-throwing -// wrappers check their slot with it: a non-zero code there can only be -// a producer panic or a marshalling failure. -func wvTrap(cErr *C.weaveffi_error) { - if cErr.code != 0 { - code, msg, _ := wvTakeError(cErr) - panic(fmt.Sprintf("weaveffi: %s (code %d)", msg, code)) - } -} - -var ( - wvCallbackMu sync.Mutex - wvCallbackSeq uint64 - wvCallbacks = map[uint64]interface{}{} - // Subscription id -> registry id, so unregister can release the Go callback. - wvListenerCtx = map[uint64]uint64{} -) - -func wvCallbackStore(v interface{}) uint64 { - wvCallbackMu.Lock() - defer wvCallbackMu.Unlock() - wvCallbackSeq++ - wvCallbacks[wvCallbackSeq] = v - return wvCallbackSeq -} - -func wvCallbackLoad(id uint64) interface{} { - wvCallbackMu.Lock() - defer wvCallbackMu.Unlock() - return wvCallbacks[id] -} - -func wvCallbackTake(id uint64) interface{} { - wvCallbackMu.Lock() - defer wvCallbackMu.Unlock() - v := wvCallbacks[id] - delete(wvCallbacks, id) - return v -} - -func wvCallbackDelete(id uint64) { - wvCallbackMu.Lock() - defer wvCallbackMu.Unlock() - delete(wvCallbacks, id) -} - -//export goWv_weaveffi_events_OnMessage_fn -func goWv_weaveffi_events_OnMessage_fn(message *C.char, context unsafe.Pointer) { - v := wvCallbackLoad(uint64(uintptr(context))) - if v == nil { - return - } - cb := v.(func(message string)) - arg0 := "" - if message != nil { - arg0 = C.GoString(message) - } - cb(arg0) -} - -// Returns a subscription id for UnregisterMessageListener. -func RegisterMessageListener(callback func(message string)) uint64 { - ctxID := wvCallbackStore(callback) - id := uint64(C.weaveffi_events_register_message_listener(C.weaveffi_events_OnMessage_fn(unsafe.Pointer(C.goWv_weaveffi_events_OnMessage_fn)), unsafe.Pointer(uintptr(ctxID)))) - wvCallbackMu.Lock() - wvListenerCtx[id] = ctxID - wvCallbackMu.Unlock() - return id -} - -// UnregisterMessageListener unregisters a listener previously registered with RegisterMessageListener. -func UnregisterMessageListener(id uint64) { - C.weaveffi_events_unregister_message_listener(C.uint64_t(id)) - wvCallbackMu.Lock() - ctxID, ok := wvListenerCtx[id] - delete(wvListenerCtx, id) - wvCallbackMu.Unlock() - if ok { - wvCallbackDelete(ctxID) - } -} - -// SendMessage: Send a message, triggering the OnMessage callback -func SendMessage(text string) { - cText := C.CString(text) - defer C.free(unsafe.Pointer(cText)) - var cErr C.weaveffi_error - C.weaveffi_events_send_message(cText, &cErr) - wvTrap(&cErr) -} - -// GetMessages: Return an iterator over all sent messages -// Returns a lazy sequence: the producer iterator is launched on first -// iteration and one producer next call runs per element. The iterator is -// destroyed exactly once, whether the sequence is exhausted or abandoned -// early; each range over the sequence launches a fresh producer iterator. -// A reported error can only be a producer bug and panics with the -// weaveffi message. -func GetMessages() iter.Seq[string] { - return func(yield func(string) bool) { - var cErr C.weaveffi_error - it := C.weaveffi_events_get_messages(&cErr) - wvTrap(&cErr) - defer C.weaveffi_events_GetMessagesIterator_destroy(it) - for { - var outItem *C.char - var iterErr C.weaveffi_error - ok := C.weaveffi_events_GetMessagesIterator_next(it, &outItem, &iterErr) != 0 - wvTrap(&iterErr) - if !ok { - return - } - item := C.GoString(outItem) - C.weaveffi_free_string(outItem) - if !yield(item) { - return - } - } - } -} - - -// END weaveffi.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_inventory__README_md.snap b/crates/weaveffi-cli/tests/snapshots/go_inventory__README_md.snap deleted file mode 100644 index 2bc2b438..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_inventory__README_md.snap +++ /dev/null @@ -1,46 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI Go Bindings - -Auto-generated Go bindings using CGo. - -## Prerequisites - -- Go >= 1.23 (the bindings return standard `iter` package sequences) -- A C compiler (gcc or clang) accessible to CGo -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, - or `weaveffi.dll`) and the C header (`weaveffi.h`) - -## Build - -1. Place `libweaveffi.so` (or the platform-specific equivalent) and - `weaveffi.h` where the linker and CGo can find them. For example, - install them into `/usr/local/lib` and `/usr/local/include`, or set - `CGO_LDFLAGS` and `CGO_CFLAGS`: - -```sh -export CGO_CFLAGS="-I/path/to/headers" -export CGO_LDFLAGS="-L/path/to/lib -lweaveffi" -``` - -2. Build or run your Go project that imports this module: - -```sh -go build ./... -``` - -## How It Works - -The generated `weaveffi.go` file uses a CGo preamble to `#include "weaveffi.h"` -and link against `-lweaveffi`. Each API function is exposed as an idiomatic Go -function that marshals arguments to C types, calls the C ABI function, and -converts the result back to Go types. Records, rich enums, optionals, lists, -and maps cross the boundary serialized in the WeaveFFI value-buffer format. -Errors are returned as Go `error` values. - - diff --git a/crates/weaveffi-cli/tests/snapshots/go_inventory__go_mod.snap b/crates/weaveffi-cli/tests/snapshots/go_inventory__go_mod.snap deleted file mode 100644 index 73a087a7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_inventory__go_mod.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -module weaveffi - -go 1.23 - -// END go.mod diff --git a/crates/weaveffi-cli/tests/snapshots/go_inventory__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_inventory__weaveffi_go.snap deleted file mode 100644 index efd246d0..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_inventory__weaveffi_go.snap +++ /dev/null @@ -1,624 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -package weaveffi - -/* -#cgo LDFLAGS: -lweaveffi -#include "weaveffi.h" -#include -*/ -import "C" - -import ( - "encoding/binary" - "fmt" - "math" - "unicode/utf8" - "unsafe" -) - -func boolToC(b bool) C._Bool { - return C._Bool(b) -} - -func cToBool(b C._Bool) bool { - return bool(b) -} - -// WeaveFFIError reports a failure crossing the C boundary that no typed -// error domain claims: an unknown code, a marshalling failure, or a -// producer panic. -type WeaveFFIError struct { - // Code is the numeric ABI error code. - Code int32 - // Message is the human-readable error message. - Message string -} - -func (e *WeaveFFIError) Error() string { - return fmt.Sprintf("weaveffi: %s (code %d)", e.Message, e.Code) -} - -// wvTakeError reads and clears a non-zero C error slot, returning its -// code, message, and a copy of its structured payload buffer (nil when -// the code declares no payload fields). -func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { - code := int32(cErr.code) - msg := "" - if cErr.message != nil { - msg = C.GoString(cErr.message) - } - var payload []byte - if cErr.payload_ptr != nil { - payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len)) - } - C.weaveffi_error_clear(cErr) - return code, msg, payload -} - -// wvTakeBoxedError reads a heap-boxed error delivered to an async -// completion callback and releases the box with weaveffi_error_free. -func wvTakeBoxedError(cErr *C.weaveffi_error) (int32, string, []byte) { - code := int32(cErr.code) - msg := "" - if cErr.message != nil { - msg = C.GoString(cErr.message) - } - var payload []byte - if cErr.payload_ptr != nil { - payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len)) - } - C.weaveffi_error_free(cErr) - return code, msg, payload -} - -func wvBrandError(code int32, message string, _ []byte) error { - return &WeaveFFIError{Code: code, Message: message} -} - -// wvTrap panics when the C error slot reports a failure. Non-throwing -// wrappers check their slot with it: a non-zero code there can only be -// a producer panic or a marshalling failure. -func wvTrap(cErr *C.weaveffi_error) { - if cErr.code != 0 { - code, msg, _ := wvTakeError(cErr) - panic(fmt.Sprintf("weaveffi: %s (code %d)", msg, code)) - } -} - -// wvWriter serializes values into the WeaveFFI 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) > uint64(^uint32(0)) { - panic("weaveffi: 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...) -} - -func (w *wvWriter) writeOptionFlag(present bool) { - w.writeBool(present) -} - -// wvReader decodes values from the WeaveFFI value-buffer format. A -// malformed buffer is a producer/consumer contract violation, so every -// read panics (the same channel a trapped producer error uses) instead -// of returning a typed domain error. -type wvReader struct { - buf []byte - pos int -} - -func wvMalformed(context string) { - panic("weaveffi: 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()) -} - -func (r *wvReader) readLen() int { - n := int(r.readU32()) - if n > len(r.buf)-r.pos { - wvMalformed("length prefix exceeds remaining buffer") - } - return n -} - -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 { - b := r.take(r.readLen(), "byte buffer") - out := make([]byte, len(b)) - copy(out, b) - return out -} - -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") - } -} - -// wvCopyBuffer copies an owned, producer-allocated value buffer into Go -// memory and releases it with weaveffi_free_bytes. -func wvCopyBuffer(ptr *C.uint8_t, length C.size_t) []byte { - if ptr == nil { - return nil - } - out := C.GoBytes(unsafe.Pointer(ptr), C.int(length)) - C.weaveffi_free_bytes(ptr, length) - return out -} - -// wvBorrowBuffer copies a borrowed value buffer into Go memory. The -// producer keeps ownership and frees it after the borrowing call returns. -func wvBorrowBuffer(ptr *C.uint8_t, length C.size_t) []byte { - if ptr == nil { - return nil - } - return C.GoBytes(unsafe.Pointer(ptr), C.int(length)) -} - -// ProductsError is a typed error reported by the `products` module. -type ProductsError struct { - // Code is the numeric ABI error code (one of the ProductsError constants). - Code int32 - // Message is the human-readable error message. - Message string -} - -func (e *ProductsError) Error() string { - return fmt.Sprintf("products: %s (code %d)", e.Message, e.Code) -} - -// ProductsError codes. -const ( - // ProductsErrorInvalidPrice price must be positive - ProductsErrorInvalidPrice int32 = 1 - // ProductsErrorProductNotFound product not found - ProductsErrorProductNotFound int32 = 2 -) - -// wvMapProducts converts a non-zero code from the `products` domain into a -// *ProductsError, falling back to the generic *WeaveFFIError for unknown codes. -func wvMapProducts(code int32, message string, payload []byte) error { - switch code { - case ProductsErrorInvalidPrice: - if message == "" { - message = "price must be positive" - } - return &ProductsError{Code: code, Message: message} - case ProductsErrorProductNotFound: - if message == "" { - message = "product not found" - } - return &ProductsError{Code: code, Message: message} - default: - return wvBrandError(code, message, payload) - } -} - -type Category int32 - -const ( - CategoryElectronics Category = 0 - CategoryClothing Category = 1 - CategoryFood Category = 2 - CategoryBooks Category = 3 -) - -type Product struct { - Id int64 - Name string - Description *string - Price float64 - Category Category - Tags []string -} - -// wvPackProduct appends v to w in the value-buffer wire format. -func wvPackProduct(w *wvWriter, v Product) { - w.writeI64(v.Id) - w.writeString(v.Name) - if v.Description == nil { - w.writeOptionFlag(false) - } else { - w.writeOptionFlag(true) - w.writeString((*v.Description)) - } - w.writeF64(v.Price) - w.writeI32(int32(v.Category)) - w.writeLen(len(v.Tags)) - for _, eTags0 := range v.Tags { - w.writeString(eTags0) - } -} - -// wvUnpackProduct decodes one Product from r. -func wvUnpackProduct(r *wvReader) Product { - var v Product - v.Id = r.readI64() - v.Name = r.readString() - if r.readOptionFlag() { - var oDescription0 string - oDescription0 = r.readString() - v.Description = &oDescription0 - } - v.Price = r.readF64() - v.Category = Category(r.readI32()) - nTags0 := r.readLen() - v.Tags = make([]string, nTags0) - for iTags0 := range v.Tags { - v.Tags[iTags0] = r.readString() - } - return v -} - -// Catalog: A product catalog owning its products -type Catalog struct { - ptr *C.weaveffi_products_Catalog -} - -func NewCatalog() *Catalog { - var cErr C.weaveffi_error - result := C.weaveffi_products_Catalog_new(&cErr) - wvTrap(&cErr) - return &Catalog{ptr: result} -} - -func (s *Catalog) AddProduct(name string, price float64, category Category) (Product, error) { - cName := C.CString(name) - defer C.free(unsafe.Pointer(cName)) - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_products_Catalog_add_product(s.ptr, cName, C.double(price), C.weaveffi_products_Category(category), &cOutLen, &cErr) - if cErr.code != 0 { - return Product{}, wvMapProducts(wvTakeError(&cErr)) - } - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult Product - goResult = wvUnpackProduct(rRes) - rRes.expectEnd() - return goResult, nil -} - -func (s *Catalog) GetProduct(id int64) (Product, error) { - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_products_Catalog_get_product(s.ptr, C.int64_t(id), &cOutLen, &cErr) - if cErr.code != 0 { - return Product{}, wvMapProducts(wvTakeError(&cErr)) - } - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult Product - goResult = wvUnpackProduct(rRes) - rRes.expectEnd() - return goResult, nil -} - -func (s *Catalog) Search(category Category) []Product { - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_products_Catalog_search(s.ptr, C.weaveffi_products_Category(category), &cOutLen, &cErr) - wvTrap(&cErr) - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult []Product - nRes0 := rRes.readLen() - goResult = make([]Product, nRes0) - for iRes0 := range goResult { - goResult[iRes0] = wvUnpackProduct(rRes) - } - rRes.expectEnd() - return goResult -} - -func (s *Catalog) UpdatePrice(id int64, price float64) (bool, error) { - var cErr C.weaveffi_error - result := C.weaveffi_products_Catalog_update_price(s.ptr, C.int64_t(id), C.double(price), &cErr) - if cErr.code != 0 { - return false, wvMapProducts(wvTakeError(&cErr)) - } - return cToBool(result), nil -} - -func (s *Catalog) Remove(id int64) bool { - var cErr C.weaveffi_error - result := C.weaveffi_products_Catalog_remove(s.ptr, C.int64_t(id), &cErr) - wvTrap(&cErr) - return cToBool(result) -} - -func (s *Catalog) Close() { - if s.ptr != nil { - C.weaveffi_products_Catalog_destroy(s.ptr) - s.ptr = nil - } -} - -// OrdersError is a typed error reported by the `orders` module. -type OrdersError struct { - // Code is the numeric ABI error code (one of the OrdersError constants). - Code int32 - // Message is the human-readable error message. - Message string -} - -func (e *OrdersError) Error() string { - return fmt.Sprintf("orders: %s (code %d)", e.Message, e.Code) -} - -// OrdersError codes. -const ( - // OrdersErrorOrderNotFound order not found - OrdersErrorOrderNotFound int32 = 1 - // OrdersErrorEmptyOrder order must contain at least one item - OrdersErrorEmptyOrder int32 = 2 -) - -// wvMapOrders converts a non-zero code from the `orders` domain into a -// *OrdersError, falling back to the generic *WeaveFFIError for unknown codes. -func wvMapOrders(code int32, message string, payload []byte) error { - switch code { - case OrdersErrorOrderNotFound: - if message == "" { - message = "order not found" - } - return &OrdersError{Code: code, Message: message} - case OrdersErrorEmptyOrder: - if message == "" { - message = "order must contain at least one item" - } - return &OrdersError{Code: code, Message: message} - default: - return wvBrandError(code, message, payload) - } -} - -type OrderItem struct { - ProductId int64 - Quantity int32 - UnitPrice float64 -} - -// wvPackOrderItem appends v to w in the value-buffer wire format. -func wvPackOrderItem(w *wvWriter, v OrderItem) { - w.writeI64(v.ProductId) - w.writeI32(v.Quantity) - w.writeF64(v.UnitPrice) -} - -// wvUnpackOrderItem decodes one OrderItem from r. -func wvUnpackOrderItem(r *wvReader) OrderItem { - var v OrderItem - v.ProductId = r.readI64() - v.Quantity = r.readI32() - v.UnitPrice = r.readF64() - return v -} - -type Order struct { - Id int64 - Items []OrderItem - Total float64 - Status string -} - -// wvPackOrder appends v to w in the value-buffer wire format. -func wvPackOrder(w *wvWriter, v Order) { - w.writeI64(v.Id) - w.writeLen(len(v.Items)) - for _, eItems0 := range v.Items { - wvPackOrderItem(w, eItems0) - } - w.writeF64(v.Total) - w.writeString(v.Status) -} - -// wvUnpackOrder decodes one Order from r. -func wvUnpackOrder(r *wvReader) Order { - var v Order - v.Id = r.readI64() - nItems0 := r.readLen() - v.Items = make([]OrderItem, nItems0) - for iItems0 := range v.Items { - v.Items[iItems0] = wvUnpackOrderItem(r) - } - v.Total = r.readF64() - v.Status = r.readString() - return v -} - -func CreateOrder(items []OrderItem) (int64, error) { - wItems := &wvWriter{} - wItems.writeLen(len(items)) - for _, eItems0 := range items { - wvPackOrderItem(wItems, eItems0) - } - var cItemsPtr *C.uint8_t - if len(wItems.buf) > 0 { - cItemsPtr = (*C.uint8_t)(unsafe.Pointer(&wItems.buf[0])) - } - var cErr C.weaveffi_error - result := C.weaveffi_orders_create_order(cItemsPtr, C.size_t(len(wItems.buf)), &cErr) - if cErr.code != 0 { - return 0, wvMapOrders(wvTakeError(&cErr)) - } - return int64(result), nil -} - -func GetOrder(id int64) (Order, error) { - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_orders_get_order(C.int64_t(id), &cOutLen, &cErr) - if cErr.code != 0 { - return Order{}, wvMapOrders(wvTakeError(&cErr)) - } - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult Order - goResult = wvUnpackOrder(rRes) - rRes.expectEnd() - return goResult, nil -} - -func CancelOrder(id int64) bool { - var cErr C.weaveffi_error - result := C.weaveffi_orders_cancel_order(C.int64_t(id), &cErr) - wvTrap(&cErr) - return cToBool(result) -} - -func AddProductToOrder(orderId int64, product Product) bool { - wProduct := &wvWriter{} - wvPackProduct(wProduct, product) - var cProductPtr *C.uint8_t - if len(wProduct.buf) > 0 { - cProductPtr = (*C.uint8_t)(unsafe.Pointer(&wProduct.buf[0])) - } - var cErr C.weaveffi_error - result := C.weaveffi_orders_add_product_to_order(C.int64_t(orderId), cProductPtr, C.size_t(len(wProduct.buf)), &cErr) - wvTrap(&cErr) - return cToBool(result) -} - - -// END weaveffi.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__weaveffi_go.snap index 273b8078..24e9d21f 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__weaveffi_go.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_kitchen_sink__weaveffi_go.snap @@ -27,6 +27,15 @@ import ( "unsafe" ) +// The ABI revision these bindings were generated against. +const wvABIVersion uint32 = 1 + +func init() { + if found := uint32(C.weaveffi_abi_version()); found != wvABIVersion { + panic(fmt.Sprintf("WeaveFFI ABI mismatch: these bindings expect revision %d but the loaded library reports revision %d", wvABIVersion, found)) + } +} + func boolToC(b bool) C._Bool { return C._Bool(b) } diff --git a/crates/weaveffi-cli/tests/snapshots/go_kvstore__README_md.snap b/crates/weaveffi-cli/tests/snapshots/go_kvstore__README_md.snap deleted file mode 100644 index 2bc2b438..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_kvstore__README_md.snap +++ /dev/null @@ -1,46 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI Go Bindings - -Auto-generated Go bindings using CGo. - -## Prerequisites - -- Go >= 1.23 (the bindings return standard `iter` package sequences) -- A C compiler (gcc or clang) accessible to CGo -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, - or `weaveffi.dll`) and the C header (`weaveffi.h`) - -## Build - -1. Place `libweaveffi.so` (or the platform-specific equivalent) and - `weaveffi.h` where the linker and CGo can find them. For example, - install them into `/usr/local/lib` and `/usr/local/include`, or set - `CGO_LDFLAGS` and `CGO_CFLAGS`: - -```sh -export CGO_CFLAGS="-I/path/to/headers" -export CGO_LDFLAGS="-L/path/to/lib -lweaveffi" -``` - -2. Build or run your Go project that imports this module: - -```sh -go build ./... -``` - -## How It Works - -The generated `weaveffi.go` file uses a CGo preamble to `#include "weaveffi.h"` -and link against `-lweaveffi`. Each API function is exposed as an idiomatic Go -function that marshals arguments to C types, calls the C ABI function, and -converts the result back to Go types. Records, rich enums, optionals, lists, -and maps cross the boundary serialized in the WeaveFFI value-buffer format. -Errors are returned as Go `error` values. - - diff --git a/crates/weaveffi-cli/tests/snapshots/go_kvstore__go_mod.snap b/crates/weaveffi-cli/tests/snapshots/go_kvstore__go_mod.snap deleted file mode 100644 index 73a087a7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_kvstore__go_mod.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -module weaveffi - -go 1.23 - -// END go.mod diff --git a/crates/weaveffi-cli/tests/snapshots/go_kvstore__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_kvstore__weaveffi_go.snap deleted file mode 100644 index 8c134bc6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/go_kvstore__weaveffi_go.snap +++ /dev/null @@ -1,761 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -package weaveffi - -/* -#cgo LDFLAGS: -lweaveffi -#include "weaveffi.h" -#include -extern void goWv_weaveffi_kv_OnEvict_fn(char* key, void* context); -extern void goWv_weaveffi_kv_Store_compact_callback(void* context, weaveffi_error* err, int64_t result); -*/ -import "C" - -import ( - "encoding/binary" - "fmt" - "iter" - "math" - "sync" - "unicode/utf8" - "unsafe" -) - -func boolToC(b bool) C._Bool { - return C._Bool(b) -} - -func cToBool(b C._Bool) bool { - return bool(b) -} - -// WeaveFFIError reports a failure crossing the C boundary that no typed -// error domain claims: an unknown code, a marshalling failure, or a -// producer panic. -type WeaveFFIError struct { - // Code is the numeric ABI error code. - Code int32 - // Message is the human-readable error message. - Message string -} - -func (e *WeaveFFIError) Error() string { - return fmt.Sprintf("weaveffi: %s (code %d)", e.Message, e.Code) -} - -// wvTakeError reads and clears a non-zero C error slot, returning its -// code, message, and a copy of its structured payload buffer (nil when -// the code declares no payload fields). -func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) { - code := int32(cErr.code) - msg := "" - if cErr.message != nil { - msg = C.GoString(cErr.message) - } - var payload []byte - if cErr.payload_ptr != nil { - payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len)) - } - C.weaveffi_error_clear(cErr) - return code, msg, payload -} - -// wvTakeBoxedError reads a heap-boxed error delivered to an async -// completion callback and releases the box with weaveffi_error_free. -func wvTakeBoxedError(cErr *C.weaveffi_error) (int32, string, []byte) { - code := int32(cErr.code) - msg := "" - if cErr.message != nil { - msg = C.GoString(cErr.message) - } - var payload []byte - if cErr.payload_ptr != nil { - payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len)) - } - C.weaveffi_error_free(cErr) - return code, msg, payload -} - -func wvBrandError(code int32, message string, _ []byte) error { - return &WeaveFFIError{Code: code, Message: message} -} - -// wvTrap panics when the C error slot reports a failure. Non-throwing -// wrappers check their slot with it: a non-zero code there can only be -// a producer panic or a marshalling failure. -func wvTrap(cErr *C.weaveffi_error) { - if cErr.code != 0 { - code, msg, _ := wvTakeError(cErr) - panic(fmt.Sprintf("weaveffi: %s (code %d)", msg, code)) - } -} - -// wvWriter serializes values into the WeaveFFI 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) > uint64(^uint32(0)) { - panic("weaveffi: 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...) -} - -func (w *wvWriter) writeOptionFlag(present bool) { - w.writeBool(present) -} - -// wvReader decodes values from the WeaveFFI value-buffer format. A -// malformed buffer is a producer/consumer contract violation, so every -// read panics (the same channel a trapped producer error uses) instead -// of returning a typed domain error. -type wvReader struct { - buf []byte - pos int -} - -func wvMalformed(context string) { - panic("weaveffi: 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()) -} - -func (r *wvReader) readLen() int { - n := int(r.readU32()) - if n > len(r.buf)-r.pos { - wvMalformed("length prefix exceeds remaining buffer") - } - return n -} - -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 { - b := r.take(r.readLen(), "byte buffer") - out := make([]byte, len(b)) - copy(out, b) - return out -} - -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") - } -} - -// wvCopyBuffer copies an owned, producer-allocated value buffer into Go -// memory and releases it with weaveffi_free_bytes. -func wvCopyBuffer(ptr *C.uint8_t, length C.size_t) []byte { - if ptr == nil { - return nil - } - out := C.GoBytes(unsafe.Pointer(ptr), C.int(length)) - C.weaveffi_free_bytes(ptr, length) - return out -} - -// wvBorrowBuffer copies a borrowed value buffer into Go memory. The -// producer keeps ownership and frees it after the borrowing call returns. -func wvBorrowBuffer(ptr *C.uint8_t, length C.size_t) []byte { - if ptr == nil { - return nil - } - return C.GoBytes(unsafe.Pointer(ptr), C.int(length)) -} - -var ( - wvCallbackMu sync.Mutex - wvCallbackSeq uint64 - wvCallbacks = map[uint64]interface{}{} - // Subscription id -> registry id, so unregister can release the Go callback. - wvListenerCtx = map[uint64]uint64{} -) - -func wvCallbackStore(v interface{}) uint64 { - wvCallbackMu.Lock() - defer wvCallbackMu.Unlock() - wvCallbackSeq++ - wvCallbacks[wvCallbackSeq] = v - return wvCallbackSeq -} - -func wvCallbackLoad(id uint64) interface{} { - wvCallbackMu.Lock() - defer wvCallbackMu.Unlock() - return wvCallbacks[id] -} - -func wvCallbackTake(id uint64) interface{} { - wvCallbackMu.Lock() - defer wvCallbackMu.Unlock() - v := wvCallbacks[id] - delete(wvCallbacks, id) - return v -} - -func wvCallbackDelete(id uint64) { - wvCallbackMu.Lock() - defer wvCallbackMu.Unlock() - delete(wvCallbacks, id) -} - -// KvError is a typed error reported by the `kv` module. -type KvError struct { - // Code is the numeric ABI error code (one of the KvError constants). - Code int32 - // Message is the human-readable error message. - Message string -} - -func (e *KvError) Error() string { - return fmt.Sprintf("kv: %s (code %d)", e.Message, e.Code) -} - -// KvError codes. -const ( - // KvErrorKeyNotFound: No entry exists for the requested key - KvErrorKeyNotFound int32 = 1001 - // KvErrorExpired: The entry exists but its TTL has elapsed - KvErrorExpired int32 = 1002 - // KvErrorStoreFull: The store has reached its capacity limit - KvErrorStoreFull int32 = 1003 - // KvErrorIoError: Underlying storage reported an I/O failure - KvErrorIoError int32 = 1004 -) - -// wvMapKv converts a non-zero code from the `kv` domain into a -// *KvError, falling back to the generic *WeaveFFIError for unknown codes. -func wvMapKv(code int32, message string, payload []byte) error { - switch code { - case KvErrorKeyNotFound: - if message == "" { - message = "key not found" - } - return &KvError{Code: code, Message: message} - case KvErrorExpired: - if message == "" { - message = "entry expired" - } - return &KvError{Code: code, Message: message} - case KvErrorStoreFull: - if message == "" { - message = "store has reached capacity" - } - return &KvError{Code: code, Message: message} - case KvErrorIoError: - if message == "" { - message = "I/O failure" - } - return &KvError{Code: code, Message: message} - default: - return wvBrandError(code, message, payload) - } -} - -// EntryKind: Persistence semantics applied to a stored entry -type EntryKind int32 - -const ( - // EntryKindVolatile: In-memory only; lost on close - EntryKindVolatile EntryKind = 0 - // EntryKindPersistent: Flushed to durable storage - EntryKindPersistent EntryKind = 1 - // EntryKindEncrypted: Persistent and encrypted at rest - EntryKindEncrypted EntryKind = 2 -) - -// Entry: A single key-value entry persisted in the store -type Entry struct { - // Id: Stable monotonic identifier assigned on insert - Id int64 - // Key: UTF-8 lookup key - Key string - // Value: Opaque binary payload - Value []byte - // CreatedAt: Unix-timestamp seconds when the entry was created - CreatedAt int64 - // ExpiresAt: Optional unix-timestamp seconds at which the entry expires - ExpiresAt *int64 - // Tags: Free-form labels attached to the entry - Tags []string - // Metadata: Arbitrary string-valued metadata pairs - Metadata map[string]string -} - -// wvPackEntry appends v to w in the value-buffer wire format. -func wvPackEntry(w *wvWriter, v Entry) { - w.writeI64(v.Id) - w.writeString(v.Key) - w.writeBytes(v.Value) - w.writeI64(v.CreatedAt) - if v.ExpiresAt == nil { - w.writeOptionFlag(false) - } else { - w.writeOptionFlag(true) - w.writeI64((*v.ExpiresAt)) - } - w.writeLen(len(v.Tags)) - for _, eTags0 := range v.Tags { - w.writeString(eTags0) - } - w.writeLen(len(v.Metadata)) - for kMetadata0, vMetadata0 := range v.Metadata { - w.writeString(kMetadata0) - w.writeString(vMetadata0) - } -} - -// wvUnpackEntry decodes one Entry from r. -func wvUnpackEntry(r *wvReader) Entry { - var v Entry - v.Id = r.readI64() - v.Key = r.readString() - v.Value = r.readBytes() - v.CreatedAt = r.readI64() - if r.readOptionFlag() { - var oExpiresAt0 int64 - oExpiresAt0 = r.readI64() - v.ExpiresAt = &oExpiresAt0 - } - nTags0 := r.readLen() - v.Tags = make([]string, nTags0) - for iTags0 := range v.Tags { - v.Tags[iTags0] = r.readString() - } - nMetadata0 := r.readLen() - v.Metadata = make(map[string]string, nMetadata0) - for iMetadata0 := 0; iMetadata0 < nMetadata0; iMetadata0++ { - var kMetadata0 string - kMetadata0 = r.readString() - var vMetadata0 string - vMetadata0 = r.readString() - v.Metadata[kMetadata0] = vMetadata0 - } - return v -} - -// Store: An in-memory key-value store owning its entries; destroying the object releases all of its state -type Store struct { - ptr *C.weaveffi_kv_Store -} - -// OpenStore: Open (or create) a store backed by the given filesystem path -func OpenStore(path string) (*Store, error) { - cPath := C.CString(path) - defer C.free(unsafe.Pointer(cPath)) - var cErr C.weaveffi_error - result := C.weaveffi_kv_Store_open(cPath, &cErr) - if cErr.code != 0 { - return nil, wvMapKv(wvTakeError(&cErr)) - } - return &Store{ptr: result}, nil -} - -// Put: Insert or replace a value, returning true on success -func (s *Store) Put(key string, value []byte, kind EntryKind, ttlSeconds *int64) (bool, error) { - cKey := C.CString(key) - defer C.free(unsafe.Pointer(cKey)) - var cValuePtr *C.uint8_t - cValueLen := C.size_t(len(value)) - if len(value) > 0 { - cValuePtr = (*C.uint8_t)(unsafe.Pointer(&value[0])) - } - wTtlSeconds := &wvWriter{} - if ttlSeconds == nil { - wTtlSeconds.writeOptionFlag(false) - } else { - wTtlSeconds.writeOptionFlag(true) - wTtlSeconds.writeI64((*ttlSeconds)) - } - var cTtlSecondsPtr *C.uint8_t - if len(wTtlSeconds.buf) > 0 { - cTtlSecondsPtr = (*C.uint8_t)(unsafe.Pointer(&wTtlSeconds.buf[0])) - } - var cErr C.weaveffi_error - result := C.weaveffi_kv_Store_put(s.ptr, cKey, cValuePtr, cValueLen, C.weaveffi_kv_EntryKind(kind), cTtlSecondsPtr, C.size_t(len(wTtlSeconds.buf)), &cErr) - if cErr.code != 0 { - return false, wvMapKv(wvTakeError(&cErr)) - } - return cToBool(result), nil -} - -// Get: Look up an entry by key; returns null if missing or expired -func (s *Store) Get(key string) (*Entry, error) { - cKey := C.CString(key) - defer C.free(unsafe.Pointer(cKey)) - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_kv_Store_get(s.ptr, cKey, &cOutLen, &cErr) - if cErr.code != 0 { - return nil, wvMapKv(wvTakeError(&cErr)) - } - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult *Entry - if rRes.readOptionFlag() { - var oRes0 Entry - oRes0 = wvUnpackEntry(rRes) - goResult = &oRes0 - } - rRes.expectEnd() - return goResult, nil -} - -// Delete: Remove the entry for the given key, returning true if it existed -func (s *Store) Delete(key string) (bool, error) { - cKey := C.CString(key) - defer C.free(unsafe.Pointer(cKey)) - var cErr C.weaveffi_error - result := C.weaveffi_kv_Store_delete(s.ptr, cKey, &cErr) - if cErr.code != 0 { - return false, wvMapKv(wvTakeError(&cErr)) - } - return cToBool(result), nil -} - -// ListKeys: Stream every key, optionally filtered by a prefix -// Returns a lazy sequence: the producer iterator is launched on first -// iteration and one producer next call runs per element. The iterator is -// destroyed exactly once, whether the sequence is exhausted or abandoned -// early; each range over the sequence launches a fresh producer iterator. -// A launch or per-element error is yielded as the final (zero value, -// error) pair, and iteration stops. -func (s *Store) ListKeys(prefix *string) iter.Seq2[string, error] { - return func(yield func(string, error) bool) { - wPrefix := &wvWriter{} - if prefix == nil { - wPrefix.writeOptionFlag(false) - } else { - wPrefix.writeOptionFlag(true) - wPrefix.writeString((*prefix)) - } - var cPrefixPtr *C.uint8_t - if len(wPrefix.buf) > 0 { - cPrefixPtr = (*C.uint8_t)(unsafe.Pointer(&wPrefix.buf[0])) - } - var cErr C.weaveffi_error - it := C.weaveffi_kv_Store_list_keys(s.ptr, cPrefixPtr, C.size_t(len(wPrefix.buf)), &cErr) - if cErr.code != 0 { - yield("", wvMapKv(wvTakeError(&cErr))) - return - } - defer C.weaveffi_kv_Store_ListKeysIterator_destroy(it) - for { - var outItem *C.char - var iterErr C.weaveffi_error - ok := C.weaveffi_kv_Store_ListKeysIterator_next(it, &outItem, &iterErr) != 0 - if iterErr.code != 0 { - yield("", wvMapKv(wvTakeError(&iterErr))) - return - } - if !ok { - return - } - item := C.GoString(outItem) - C.weaveffi_free_string(outItem) - if !yield(item, nil) { - return - } - } - } -} - -// Count: Return the number of live entries in the store -func (s *Store) Count() int64 { - var cErr C.weaveffi_error - result := C.weaveffi_kv_Store_count(s.ptr, &cErr) - wvTrap(&cErr) - return int64(result) -} - -// Clear: Drop every entry from the store -func (s *Store) Clear() { - var cErr C.weaveffi_error - C.weaveffi_kv_Store_clear(s.ptr, &cErr) - wvTrap(&cErr) -} - -type wvOutcomeKvStoreCompact struct { - val int64 - err error -} - -//export goWv_weaveffi_kv_Store_compact_callback -func goWv_weaveffi_kv_Store_compact_callback(context unsafe.Pointer, err *C.weaveffi_error, result C.int64_t) { - v := wvCallbackTake(uint64(uintptr(context))) - if v == nil { - return - } - ch := v.(chan wvOutcomeKvStoreCompact) - if err != nil && err.code != 0 { - ch <- wvOutcomeKvStoreCompact{err: wvMapKv(wvTakeBoxedError(err))} - return - } - ch <- wvOutcomeKvStoreCompact{val: int64(result)} -} - -// Compact: Reclaim space asynchronously; returns the number of bytes reclaimed -// Blocks the calling goroutine until the async producer completes. -func (s *Store) Compact() (int64, error) { - ch := make(chan wvOutcomeKvStoreCompact, 1) - ctxID := wvCallbackStore(ch) - C.weaveffi_kv_Store_compact_async(s.ptr, nil, C.weaveffi_kv_Store_compact_callback(unsafe.Pointer(C.goWv_weaveffi_kv_Store_compact_callback)), unsafe.Pointer(uintptr(ctxID))) - outcome := <-ch - if outcome.err != nil { - return 0, outcome.err - } - return outcome.val, nil -} - -// LegacyPut: Legacy single-shot put kept for compatibility -// Deprecated: use put() with explicit kind -func (s *Store) LegacyPut(key string, value []byte) (bool, error) { - cKey := C.CString(key) - defer C.free(unsafe.Pointer(cKey)) - var cValuePtr *C.uint8_t - cValueLen := C.size_t(len(value)) - if len(value) > 0 { - cValuePtr = (*C.uint8_t)(unsafe.Pointer(&value[0])) - } - var cErr C.weaveffi_error - result := C.weaveffi_kv_Store_legacy_put(s.ptr, cKey, cValuePtr, cValueLen, &cErr) - if cErr.code != 0 { - return false, wvMapKv(wvTakeError(&cErr)) - } - return cToBool(result), nil -} - -// StoreDefaultCapacity: The largest number of live entries one store will hold -func StoreDefaultCapacity() int64 { - var cErr C.weaveffi_error - result := C.weaveffi_kv_Store_default_capacity(&cErr) - wvTrap(&cErr) - return int64(result) -} - -func (s *Store) Close() { - if s.ptr != nil { - C.weaveffi_kv_Store_destroy(s.ptr) - s.ptr = nil - } -} - -//export goWv_weaveffi_kv_OnEvict_fn -func goWv_weaveffi_kv_OnEvict_fn(key *C.char, context unsafe.Pointer) { - v := wvCallbackLoad(uint64(uintptr(context))) - if v == nil { - return - } - cb := v.(func(key string)) - arg0 := "" - if key != nil { - arg0 = C.GoString(key) - } - cb(arg0) -} - -// RegisterEvictionListener: Subscribe to per-key eviction notifications -// Returns a subscription id for UnregisterEvictionListener. -func RegisterEvictionListener(callback func(key string)) uint64 { - ctxID := wvCallbackStore(callback) - id := uint64(C.weaveffi_kv_register_eviction_listener(C.weaveffi_kv_OnEvict_fn(unsafe.Pointer(C.goWv_weaveffi_kv_OnEvict_fn)), unsafe.Pointer(uintptr(ctxID)))) - wvCallbackMu.Lock() - wvListenerCtx[id] = ctxID - wvCallbackMu.Unlock() - return id -} - -// UnregisterEvictionListener unregisters a listener previously registered with RegisterEvictionListener. -func UnregisterEvictionListener(id uint64) { - C.weaveffi_kv_unregister_eviction_listener(C.uint64_t(id)) - wvCallbackMu.Lock() - ctxID, ok := wvListenerCtx[id] - delete(wvListenerCtx, id) - wvCallbackMu.Unlock() - if ok { - wvCallbackDelete(ctxID) - } -} - -// Stats: Aggregate store statistics -type Stats struct { - // TotalEntries: Number of live entries - TotalEntries int64 - // TotalBytes: Sum of all value byte lengths - TotalBytes int64 - // ExpiredEntries: Number of entries past their TTL but not yet evicted - ExpiredEntries int64 -} - -// wvPackStats appends v to w in the value-buffer wire format. -func wvPackStats(w *wvWriter, v Stats) { - w.writeI64(v.TotalEntries) - w.writeI64(v.TotalBytes) - w.writeI64(v.ExpiredEntries) -} - -// wvUnpackStats decodes one Stats from r. -func wvUnpackStats(r *wvReader) Stats { - var v Stats - v.TotalEntries = r.readI64() - v.TotalBytes = r.readI64() - v.ExpiredEntries = r.readI64() - return v -} - -// GetStats: Snapshot the current store statistics -func GetStats(store *Store) (Stats, error) { - var cOutLen C.size_t - var cErr C.weaveffi_error - result := C.weaveffi_kv_stats_get_stats(store.ptr, &cOutLen, &cErr) - if cErr.code != 0 { - return Stats{}, wvMapKv(wvTakeError(&cErr)) - } - rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)} - var goResult Stats - goResult = wvUnpackStats(rRes) - rRes.expectEnd() - return goResult, nil -} - - -// END weaveffi.go diff --git a/crates/weaveffi-cli/tests/snapshots/go_nested_modules__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__weaveffi_go.snap index 1681a6ab..8d4e0641 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_nested_modules__weaveffi_go.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_nested_modules__weaveffi_go.snap @@ -22,6 +22,15 @@ import ( "unsafe" ) +// The ABI revision these bindings were generated against. +const wvABIVersion uint32 = 1 + +func init() { + if found := uint32(C.weaveffi_abi_version()); found != wvABIVersion { + panic(fmt.Sprintf("WeaveFFI ABI mismatch: these bindings expect revision %d but the loaded library reports revision %d", wvABIVersion, found)) + } +} + // WeaveFFIError reports a failure crossing the C boundary that no typed // error domain claims: an unknown code, a marshalling failure, or a // producer panic. diff --git a/crates/weaveffi-cli/tests/snapshots/go_shapes__weaveffi_go.snap b/crates/weaveffi-cli/tests/snapshots/go_shapes__weaveffi_go.snap index abf4fa77..69cb1b41 100644 --- a/crates/weaveffi-cli/tests/snapshots/go_shapes__weaveffi_go.snap +++ b/crates/weaveffi-cli/tests/snapshots/go_shapes__weaveffi_go.snap @@ -22,6 +22,15 @@ import ( "unsafe" ) +// The ABI revision these bindings were generated against. +const wvABIVersion uint32 = 1 + +func init() { + if found := uint32(C.weaveffi_abi_version()); found != wvABIVersion { + panic(fmt.Sprintf("WeaveFFI ABI mismatch: these bindings expect revision %d but the loaded library reports revision %d", wvABIVersion, found)) + } +} + // WeaveFFIError reports a failure crossing the C boundary that no typed // error domain claims: an unknown code, a marshalling failure, or a // producer panic. diff --git a/crates/weaveffi-cli/tests/snapshots/node_async_demo__index_js.snap b/crates/weaveffi-cli/tests/snapshots/node_async_demo__index_js.snap deleted file mode 100644 index f027f27a..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_async_demo__index_js.snap +++ /dev/null @@ -1,233 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// The WEAVEFFI_ADDON environment variable overrides the addon location -// (an absolute path to the built .node file); otherwise prefer the -// default node-gyp output path and fall back to a prebuilt index.node -// placed next to this file. -let addon; -if (process.env.WEAVEFFI_ADDON) { - addon = require(process.env.WEAVEFFI_ADDON); -} else { - try { - addon = require('./build/Release/weaveffi.node'); - } catch (e) { - addon = require('./index.node'); - } -} - -// Re-export every native binding, then layer the idiomatic wrappers -// (error classes, interface classes, buffer pack/unpack, function -// wrappers) on top. -const wv = {}; -for (const _name of Object.getOwnPropertyNames(addon)) { - wv[_name] = addon[_name]; -} - -class WeaveFFIError extends Error { - constructor(code, message) { - super('(' + code + ') ' + (message || '')); - this.name = 'WeaveFFIError'; - this.code = code; - this.errorMessage = message || ''; - } -} -wv.WeaveFFIError = WeaveFFIError; -function __generic(code, message) { - return new WeaveFFIError(code, message); -} -function __rebrand(e, map) { - return e && typeof e.code === 'number' ? map(e.code, e.message, e.payload) : e; -} -function __invoke(fn, args, map) { - try { - return fn.apply(null, args); - } catch (e) { - throw __rebrand(e, map); - } -} -function __invokeAsync(fn, args, map) { - return fn.apply(null, args).catch((e) => { - throw __rebrand(e, map); - }); -} - -// --- Private value-buffer runtime (WeaveFFI wire format) -------------------- -// Little-endian, packed, no alignment. Decoders reject truncated buffers, -// invalid bool/flag bytes, hostile length prefixes, and trailing bytes. -const __utf8 = new TextDecoder('utf-8', { fatal: true }); -function __bufferError(what) { - return new WeaveFFIError(-2, 'malformed value buffer: ' + what); -} -class __Writer { - constructor() { - this._buf = Buffer.alloc(256); - this._len = 0; - } - _reserve(n) { - if (this._len + n <= this._buf.length) return; - let cap = this._buf.length; - while (cap < this._len + n) cap *= 2; - const grown = Buffer.alloc(cap); - this._buf.copy(grown, 0, 0, this._len); - this._buf = grown; - } - bool(v) { this._reserve(1); this._buf[this._len++] = v ? 1 : 0; } - i8(v) { this._reserve(1); this._buf.writeInt8(v, this._len); this._len += 1; } - u8(v) { this._reserve(1); this._buf.writeUInt8(v, this._len); this._len += 1; } - i16(v) { this._reserve(2); this._buf.writeInt16LE(v, this._len); this._len += 2; } - u16(v) { this._reserve(2); this._buf.writeUInt16LE(v, this._len); this._len += 2; } - i32(v) { this._reserve(4); this._buf.writeInt32LE(v, this._len); this._len += 4; } - u32(v) { this._reserve(4); this._buf.writeUInt32LE(v, this._len); this._len += 4; } - i64(v) { this._reserve(8); this._buf.writeBigInt64LE(BigInt(v), this._len); this._len += 8; } - u64(v) { this._reserve(8); this._buf.writeBigUInt64LE(BigInt(v), this._len); this._len += 8; } - f32(v) { this._reserve(4); this._buf.writeFloatLE(v, this._len); this._len += 4; } - f64(v) { this._reserve(8); this._buf.writeDoubleLE(v, this._len); this._len += 8; } - str(v) { - const b = Buffer.from(String(v), 'utf8'); - this.u32(b.length); - this._reserve(b.length); - b.copy(this._buf, this._len); - this._len += b.length; - } - bytes(v) { - this.u32(v.length); - this._reserve(v.length); - this._buf.set(v, this._len); - this._len += v.length; - } - finish() { return this._buf.subarray(0, this._len); } -} -class __Reader { - constructor(buf) { - this._buf = Buffer.isBuffer(buf) ? buf : Buffer.from(buf); - this._pos = 0; - } - _take(n, what) { - if (this._pos + n > this._buf.length) throw __bufferError(what); - const at = this._pos; - this._pos += n; - return at; - } - bool() { - const b = this._buf[this._take(1, 'bool')]; - if (b > 1) throw __bufferError('bool byte out of range'); - return b === 1; - } - i8() { return this._buf.readInt8(this._take(1, 'i8')); } - u8() { return this._buf.readUInt8(this._take(1, 'u8')); } - i16() { return this._buf.readInt16LE(this._take(2, 'i16')); } - u16() { return this._buf.readUInt16LE(this._take(2, 'u16')); } - i32() { return this._buf.readInt32LE(this._take(4, 'i32')); } - u32() { return this._buf.readUInt32LE(this._take(4, 'u32')); } - i64() { return this._buf.readBigInt64LE(this._take(8, 'i64')); } - u64() { return this._buf.readBigUInt64LE(this._take(8, 'u64')); } - f32() { return this._buf.readFloatLE(this._take(4, 'f32')); } - f64() { return this._buf.readDoubleLE(this._take(8, 'f64')); } - len() { - const n = this.u32(); - if (n > this._buf.length - this._pos) throw __bufferError('length prefix exceeds remaining bytes'); - return n; - } - str() { - const n = this.len(); - const at = this._take(n, 'string bytes'); - try { - return __utf8.decode(this._buf.subarray(at, at + n)); - } catch (e) { - throw __bufferError('string is not valid UTF-8'); - } - } - bytes() { - const n = this.len(); - const at = this._take(n, 'byte buffer'); - return Buffer.from(this._buf.subarray(at, at + n)); - } - end() { - if (this._pos !== this._buf.length) throw __bufferError('trailing bytes after value'); - } -} -function __encode(f, v) { const w = new __Writer(); f(w, v); return w.finish(); } -function __decode(f, b) { const r = new __Reader(b); const v = f(r); r.end(); return v; } -function __wOpt(w, v, f) { if (v === null || v === undefined) { w.bool(false); } else { w.bool(true); f(w, v); } } -function __rOpt(r, f) { return r.bool() ? f(r) : null; } -function __wList(w, v, f) { w.u32(v.length); for (const e of v) f(w, e); } -function __rList(r, f) { - const n = r.len(); - const out = []; - for (let i = 0; i < n; i++) out.push(f(r)); - return out; -} -function __wMap(w, v, kf, vf) { - const keys = Object.keys(v); - w.u32(keys.length); - for (const k of keys) { kf(w, k); vf(w, v[k]); } -} -function __rMap(r, kf, vf) { - const n = r.len(); - const out = {}; - for (let i = 0; i < n; i++) { - const k = kf(r); - out[k] = vf(r); - } - return out; -} - -function __packTaskResult(w, v) { - w.i64(v.id); - w.str(v.value); - w.bool(v.success); -} -function __unpackTaskResult(r) { - return { - id: Number(r.i64()), - value: r.str(), - success: r.bool(), - }; -} - -class TaskError extends WeaveFFIError { - constructor(code, message) { - super(code, message); - this.name = 'TaskError'; - } -} -wv.TaskError = TaskError; -class InvalidNameError extends TaskError { - constructor(message) { - super(1, message || 'task name must not be empty'); - this.name = 'InvalidNameError'; - } -} -InvalidNameError.CODE = 1; -wv.InvalidNameError = InvalidNameError; -const __tasksErrorCodes = Object.freeze({ 1: InvalidNameError }); -function __tasksErrorFrom(code, message, payload) { - const _cls = __tasksErrorCodes[code]; - const _err = _cls === undefined ? new WeaveFFIError(code, message) : new _cls(message); - return _err; -} - -wv.runTask = function (name) { - return __invokeAsync(addon.runTask, [name], __tasksErrorFrom).then((_r) => __decode(__unpackTaskResult, _r)); -}; -wv.runBatch = function (names) { - return __invokeAsync(addon.runBatch, [__encode((w, v) => __wList(w, v, (w, v) => w.str(v)), names)], __generic).then((_r) => __decode((r) => __rList(r, __unpackTaskResult), _r)); -}; -wv.cancelTask = function (id) { - return __invoke(addon.cancelTask, [id], __generic); -}; -wv.runNTasks = function (n) { - return __invokeAsync(addon.runNTasks, [n], __generic); -}; -wv.activeCallbacks = function () { - return __invoke(addon.activeCallbacks, [], __generic); -}; - -module.exports = wv; - -// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_async_demo__types_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_async_demo__types_d_ts.snap deleted file mode 100644 index cb02e6ec..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_async_demo__types_d_ts.snap +++ /dev/null @@ -1,57 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// Generated types for WeaveFFI functions -/** - * Base class of every error thrown by these bindings. Non-throwing - * functions reject or throw it directly for panics and marshalling - * failures; throwing functions surface a module domain subclass. - */ -export class WeaveFFIError extends Error { - /** The numeric ABI error code. */ - code: number; - /** The raw producer message, without the code prefix. */ - errorMessage: string; - constructor(code: number, message: string); -} -/** Typed errors reported by the `tasks` module's throwing functions. */ -export class TaskError extends WeaveFFIError { - constructor(code: number, message: string); -} -export class InvalidNameError extends TaskError { - static readonly CODE: 1; - constructor(message?: string); -} -export interface TaskResult { - id: number; - value: string; - success: boolean; -} -// module tasks -/** - * Maps to C function: weaveffi_tasks_run_task - * @throws {TaskError} - */ -export function runTask(name: string): Promise -/** - * Maps to C function: weaveffi_tasks_run_batch - */ -export function runBatch(names: string[]): Promise -/** - * Maps to C function: weaveffi_tasks_cancel_task - */ -export function cancelTask(id: number): boolean -/** - * Maps to C function: weaveffi_tasks_run_n_tasks - */ -export function runNTasks(n: number): Promise -/** - * Maps to C function: weaveffi_tasks_active_callbacks - */ -export function activeCallbacks(): number - -// END types.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/node_async_demo__weaveffi_addon_c.snap b/crates/weaveffi-cli/tests/snapshots/node_async_demo__weaveffi_addon_c.snap deleted file mode 100644 index b018ccb8..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_async_demo__weaveffi_addon_c.snap +++ /dev/null @@ -1,268 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include -#include "weaveffi.h" -#include -#include - -static napi_value weaveffi_napi_error_value(napi_env env, int32_t code, const char* message, const uint8_t* payload_ptr, size_t payload_len) { - napi_value msg; - napi_create_string_utf8(env, message ? message : "", NAPI_AUTO_LENGTH, &msg); - napi_value err; - napi_create_error(env, NULL, msg, &err); - napi_value code_val; - napi_create_int32(env, code, &code_val); - napi_set_named_property(env, err, "code", code_val); - if (payload_ptr != NULL) { - napi_value payload_val; - napi_create_buffer_copy(env, payload_len, payload_ptr, NULL, &payload_val); - napi_set_named_property(env, err, "payload", payload_val); - } - return err; -} - -typedef struct { - napi_deferred deferred; - napi_threadsafe_function tsfn; - int32_t err_code; - char* err_msg; - uint8_t* err_payload; - size_t err_payload_len; - uint8_t* result; - size_t result_len; -} weaveffi_tasks_run_task_napi_actx; - -static void weaveffi_tasks_run_task_napi_cb(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { - weaveffi_tasks_run_task_napi_actx* ctx = (weaveffi_tasks_run_task_napi_actx*)context; - if (err != NULL && err->code != 0) { - ctx->err_code = err->code; - ctx->err_msg = err->message ? strdup(err->message) : strdup("unknown error"); - ctx->err_payload_len = err->payload_len; - if (err->payload_ptr != NULL && err->payload_len > 0) { ctx->err_payload = (uint8_t*)malloc(err->payload_len); memcpy(ctx->err_payload, err->payload_ptr, err->payload_len); } - } else { - ctx->result_len = result_len; - if (result_ptr != NULL && result_len > 0) { ctx->result = (uint8_t*)malloc(result_len); memcpy(ctx->result, result_ptr, result_len); } - weaveffi_free_bytes((uint8_t*)result_ptr, result_len); - } - weaveffi_error_free(err); - napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); -} - -static void weaveffi_tasks_run_task_napi_settle(napi_env env, napi_value js_cb, void* context, void* data) { - (void)js_cb; - (void)context; - weaveffi_tasks_run_task_napi_actx* ctx = (weaveffi_tasks_run_task_napi_actx*)data; - if (env != NULL) { - if (ctx->err_code != 0) { - napi_value err_obj = weaveffi_napi_error_value(env, ctx->err_code, ctx->err_msg, ctx->err_payload, ctx->err_payload_len); - napi_reject_deferred(env, ctx->deferred, err_obj); - } else { - napi_value val; - napi_create_buffer_copy(env, ctx->result_len, ctx->result ? (const void*)ctx->result : (const void*)"", NULL, &val); - napi_resolve_deferred(env, ctx->deferred, val); - } - } - free(ctx->err_msg); - free(ctx->err_payload); - free(ctx->result); - napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release); - free(ctx); -} - -static napi_value Napi_weaveffi_tasks_run_task(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - size_t name_len; - napi_get_value_string_utf8(env, args[0], NULL, 0, &name_len); - char* name = (char*)malloc(name_len + 1); - napi_get_value_string_utf8(env, args[0], name, name_len + 1, &name_len); - weaveffi_tasks_run_task_napi_actx* ctx = (weaveffi_tasks_run_task_napi_actx*)calloc(1, sizeof(weaveffi_tasks_run_task_napi_actx)); - napi_value promise; - napi_create_promise(env, &ctx->deferred, &promise); - napi_value resource_name; - napi_create_string_utf8(env, "weaveffi_tasks_run_task", NAPI_AUTO_LENGTH, &resource_name); - napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_tasks_run_task_napi_settle, &ctx->tsfn); - weaveffi_tasks_run_task_async(name, weaveffi_tasks_run_task_napi_cb, ctx); - free(name); - return promise; -} - -typedef struct { - napi_deferred deferred; - napi_threadsafe_function tsfn; - int32_t err_code; - char* err_msg; - uint8_t* err_payload; - size_t err_payload_len; - uint8_t* result; - size_t result_len; -} weaveffi_tasks_run_batch_napi_actx; - -static void weaveffi_tasks_run_batch_napi_cb(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { - weaveffi_tasks_run_batch_napi_actx* ctx = (weaveffi_tasks_run_batch_napi_actx*)context; - if (err != NULL && err->code != 0) { - ctx->err_code = err->code; - ctx->err_msg = err->message ? strdup(err->message) : strdup("unknown error"); - ctx->err_payload_len = err->payload_len; - if (err->payload_ptr != NULL && err->payload_len > 0) { ctx->err_payload = (uint8_t*)malloc(err->payload_len); memcpy(ctx->err_payload, err->payload_ptr, err->payload_len); } - } else { - ctx->result_len = result_len; - if (result_ptr != NULL && result_len > 0) { ctx->result = (uint8_t*)malloc(result_len); memcpy(ctx->result, result_ptr, result_len); } - weaveffi_free_bytes((uint8_t*)result_ptr, result_len); - } - weaveffi_error_free(err); - napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); -} - -static void weaveffi_tasks_run_batch_napi_settle(napi_env env, napi_value js_cb, void* context, void* data) { - (void)js_cb; - (void)context; - weaveffi_tasks_run_batch_napi_actx* ctx = (weaveffi_tasks_run_batch_napi_actx*)data; - if (env != NULL) { - if (ctx->err_code != 0) { - napi_value err_obj = weaveffi_napi_error_value(env, ctx->err_code, ctx->err_msg, ctx->err_payload, ctx->err_payload_len); - napi_reject_deferred(env, ctx->deferred, err_obj); - } else { - napi_value val; - napi_create_buffer_copy(env, ctx->result_len, ctx->result ? (const void*)ctx->result : (const void*)"", NULL, &val); - napi_resolve_deferred(env, ctx->deferred, val); - } - } - free(ctx->err_msg); - free(ctx->err_payload); - free(ctx->result); - napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release); - free(ctx); -} - -static napi_value Napi_weaveffi_tasks_run_batch(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - void* names_raw; - size_t names_len; - napi_get_buffer_info(env, args[0], &names_raw, &names_len); - weaveffi_tasks_run_batch_napi_actx* ctx = (weaveffi_tasks_run_batch_napi_actx*)calloc(1, sizeof(weaveffi_tasks_run_batch_napi_actx)); - napi_value promise; - napi_create_promise(env, &ctx->deferred, &promise); - napi_value resource_name; - napi_create_string_utf8(env, "weaveffi_tasks_run_batch", NAPI_AUTO_LENGTH, &resource_name); - napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_tasks_run_batch_napi_settle, &ctx->tsfn); - weaveffi_tasks_run_batch_async((const uint8_t*)names_raw, names_len, weaveffi_tasks_run_batch_napi_cb, ctx); - return promise; -} - -static napi_value Napi_weaveffi_tasks_cancel_task(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t id; - napi_get_value_int64(env, args[0], &id); - weaveffi_error err = {0}; - bool result = weaveffi_tasks_cancel_task(id, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_get_boolean(env, result, &ret); - return ret; -} - -typedef struct { - napi_deferred deferred; - napi_threadsafe_function tsfn; - int32_t err_code; - char* err_msg; - uint8_t* err_payload; - size_t err_payload_len; - int32_t result; -} weaveffi_tasks_run_n_tasks_napi_actx; - -static void weaveffi_tasks_run_n_tasks_napi_cb(void* context, weaveffi_error* err, int32_t result) { - weaveffi_tasks_run_n_tasks_napi_actx* ctx = (weaveffi_tasks_run_n_tasks_napi_actx*)context; - if (err != NULL && err->code != 0) { - ctx->err_code = err->code; - ctx->err_msg = err->message ? strdup(err->message) : strdup("unknown error"); - ctx->err_payload_len = err->payload_len; - if (err->payload_ptr != NULL && err->payload_len > 0) { ctx->err_payload = (uint8_t*)malloc(err->payload_len); memcpy(ctx->err_payload, err->payload_ptr, err->payload_len); } - } else { - ctx->result = result; - } - weaveffi_error_free(err); - napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); -} - -static void weaveffi_tasks_run_n_tasks_napi_settle(napi_env env, napi_value js_cb, void* context, void* data) { - (void)js_cb; - (void)context; - weaveffi_tasks_run_n_tasks_napi_actx* ctx = (weaveffi_tasks_run_n_tasks_napi_actx*)data; - if (env != NULL) { - if (ctx->err_code != 0) { - napi_value err_obj = weaveffi_napi_error_value(env, ctx->err_code, ctx->err_msg, ctx->err_payload, ctx->err_payload_len); - napi_reject_deferred(env, ctx->deferred, err_obj); - } else { - napi_value val; - napi_create_int32(env, ctx->result, &val); - napi_resolve_deferred(env, ctx->deferred, val); - } - } - free(ctx->err_msg); - free(ctx->err_payload); - napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release); - free(ctx); -} - -static napi_value Napi_weaveffi_tasks_run_n_tasks(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int32_t n; - napi_get_value_int32(env, args[0], &n); - weaveffi_tasks_run_n_tasks_napi_actx* ctx = (weaveffi_tasks_run_n_tasks_napi_actx*)calloc(1, sizeof(weaveffi_tasks_run_n_tasks_napi_actx)); - napi_value promise; - napi_create_promise(env, &ctx->deferred, &promise); - napi_value resource_name; - napi_create_string_utf8(env, "weaveffi_tasks_run_n_tasks", NAPI_AUTO_LENGTH, &resource_name); - napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_tasks_run_n_tasks_napi_settle, &ctx->tsfn); - weaveffi_tasks_run_n_tasks_async(n, weaveffi_tasks_run_n_tasks_napi_cb, ctx); - return promise; -} - -static napi_value Napi_weaveffi_tasks_active_callbacks(napi_env env, napi_callback_info info) { - size_t argc = 0; - napi_get_cb_info(env, info, &argc, NULL, NULL, NULL); - weaveffi_error err = {0}; - int64_t result = weaveffi_tasks_active_callbacks(&err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_int64(env, result, &ret); - return ret; -} - -static napi_value Init(napi_env env, napi_value exports) { - napi_property_descriptor props[] = { - { "runTask", NULL, Napi_weaveffi_tasks_run_task, NULL, NULL, NULL, napi_default, NULL }, - { "runBatch", NULL, Napi_weaveffi_tasks_run_batch, NULL, NULL, NULL, napi_default, NULL }, - { "cancelTask", NULL, Napi_weaveffi_tasks_cancel_task, NULL, NULL, NULL, napi_default, NULL }, - { "runNTasks", NULL, Napi_weaveffi_tasks_run_n_tasks, NULL, NULL, NULL, napi_default, NULL }, - { "activeCallbacks", NULL, Napi_weaveffi_tasks_active_callbacks, NULL, NULL, NULL, napi_default, NULL }, - }; - napi_define_properties(env, exports, 5, props); - return exports; -} - -NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) - -// END weaveffi_addon.c diff --git a/crates/weaveffi-cli/tests/snapshots/node_calculator__binding_gyp.snap b/crates/weaveffi-cli/tests/snapshots/node_calculator__binding_gyp.snap deleted file mode 100644 index 3997b5bb..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_calculator__binding_gyp.snap +++ /dev/null @@ -1,19 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -{ - "targets": [ - { - "target_name": "weaveffi", - "sources": ["weaveffi_addon.c"], - "include_dirs": ["../c"], - "libraries": ["-lweaveffi"] - } - ] -} - -# END binding.gyp diff --git a/crates/weaveffi-cli/tests/snapshots/node_calculator__index_js.snap b/crates/weaveffi-cli/tests/snapshots/node_calculator__index_js.snap deleted file mode 100644 index d9ad3433..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_calculator__index_js.snap +++ /dev/null @@ -1,96 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// The WEAVEFFI_ADDON environment variable overrides the addon location -// (an absolute path to the built .node file); otherwise prefer the -// default node-gyp output path and fall back to a prebuilt index.node -// placed next to this file. -let addon; -if (process.env.WEAVEFFI_ADDON) { - addon = require(process.env.WEAVEFFI_ADDON); -} else { - try { - addon = require('./build/Release/weaveffi.node'); - } catch (e) { - addon = require('./index.node'); - } -} - -// Re-export every native binding, then layer the idiomatic wrappers -// (error classes, interface classes, buffer pack/unpack, function -// wrappers) on top. -const wv = {}; -for (const _name of Object.getOwnPropertyNames(addon)) { - wv[_name] = addon[_name]; -} - -class WeaveFFIError extends Error { - constructor(code, message) { - super('(' + code + ') ' + (message || '')); - this.name = 'WeaveFFIError'; - this.code = code; - this.errorMessage = message || ''; - } -} -wv.WeaveFFIError = WeaveFFIError; -function __generic(code, message) { - return new WeaveFFIError(code, message); -} -function __rebrand(e, map) { - return e && typeof e.code === 'number' ? map(e.code, e.message, e.payload) : e; -} -function __invoke(fn, args, map) { - try { - return fn.apply(null, args); - } catch (e) { - throw __rebrand(e, map); - } -} -function __invokeAsync(fn, args, map) { - return fn.apply(null, args).catch((e) => { - throw __rebrand(e, map); - }); -} - -class CalcError extends WeaveFFIError { - constructor(code, message) { - super(code, message); - this.name = 'CalcError'; - } -} -wv.CalcError = CalcError; -class DivisionByZeroError extends CalcError { - constructor(message) { - super(1, message || 'division by zero'); - this.name = 'DivisionByZeroError'; - } -} -DivisionByZeroError.CODE = 1; -wv.DivisionByZeroError = DivisionByZeroError; -const __calculatorErrorCodes = Object.freeze({ 1: DivisionByZeroError }); -function __calculatorErrorFrom(code, message, payload) { - const _cls = __calculatorErrorCodes[code]; - const _err = _cls === undefined ? new WeaveFFIError(code, message) : new _cls(message); - return _err; -} - -wv.add = function (a, b) { - return __invoke(addon.add, [a, b], __generic); -}; -wv.mul = function (a, b) { - return __invoke(addon.mul, [a, b], __generic); -}; -wv.div = function (a, b) { - return __invoke(addon.div, [a, b], __calculatorErrorFrom); -}; -wv.echo = function (s) { - return __invoke(addon.echo, [s], __generic); -}; - -module.exports = wv; - -// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_calculator__package_json.snap b/crates/weaveffi-cli/tests/snapshots/node_calculator__package_json.snap deleted file mode 100644 index 6fd366d2..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_calculator__package_json.snap +++ /dev/null @@ -1,17 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -{ - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", - "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", - "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", - "main": "index.js", - "types": "types.d.ts", - "gypfile": true, - "scripts": { - "install": "node-gyp rebuild" - } -} diff --git a/crates/weaveffi-cli/tests/snapshots/node_calculator__types_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_calculator__types_d_ts.snap deleted file mode 100644 index bad89eb5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_calculator__types_d_ts.snap +++ /dev/null @@ -1,52 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// Generated types for WeaveFFI functions -/** - * Base class of every error thrown by these bindings. Non-throwing - * functions reject or throw it directly for panics and marshalling - * failures; throwing functions surface a module domain subclass. - */ -export class WeaveFFIError extends Error { - /** The numeric ABI error code. */ - code: number; - /** The raw producer message, without the code prefix. */ - errorMessage: string; - constructor(code: number, message: string); -} -/** Typed errors reported by the `calculator` module's throwing functions. */ -export class CalcError extends WeaveFFIError { - constructor(code: number, message: string); -} -export class DivisionByZeroError extends CalcError { - static readonly CODE: 1; - constructor(message?: string); -} -// module calculator -/** - * Add two integers - * Maps to C function: weaveffi_calculator_add - */ -export function add(a: number, b: number): number -/** - * Multiply two integers - * Maps to C function: weaveffi_calculator_mul - */ -export function mul(a: number, b: number): number -/** - * Divide two integers, failing on a zero divisor - * Maps to C function: weaveffi_calculator_div - * @throws {CalcError} - */ -export function div(a: number, b: number): number -/** - * Echo a string back - * Maps to C function: weaveffi_calculator_echo - */ -export function echo(s: string): string - -// END types.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/node_calculator__weaveffi_addon_c.snap b/crates/weaveffi-cli/tests/snapshots/node_calculator__weaveffi_addon_c.snap deleted file mode 100644 index d06a444f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_calculator__weaveffi_addon_c.snap +++ /dev/null @@ -1,124 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include -#include "weaveffi.h" -#include -#include - -static napi_value weaveffi_napi_error_value(napi_env env, int32_t code, const char* message, const uint8_t* payload_ptr, size_t payload_len) { - napi_value msg; - napi_create_string_utf8(env, message ? message : "", NAPI_AUTO_LENGTH, &msg); - napi_value err; - napi_create_error(env, NULL, msg, &err); - napi_value code_val; - napi_create_int32(env, code, &code_val); - napi_set_named_property(env, err, "code", code_val); - if (payload_ptr != NULL) { - napi_value payload_val; - napi_create_buffer_copy(env, payload_len, payload_ptr, NULL, &payload_val); - napi_set_named_property(env, err, "payload", payload_val); - } - return err; -} - -static napi_value Napi_weaveffi_calculator_add(napi_env env, napi_callback_info info) { - size_t argc = 2; - napi_value args[2]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int32_t a; - napi_get_value_int32(env, args[0], &a); - int32_t b; - napi_get_value_int32(env, args[1], &b); - weaveffi_error err = {0}; - int32_t result = weaveffi_calculator_add(a, b, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_int32(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_calculator_mul(napi_env env, napi_callback_info info) { - size_t argc = 2; - napi_value args[2]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int32_t a; - napi_get_value_int32(env, args[0], &a); - int32_t b; - napi_get_value_int32(env, args[1], &b); - weaveffi_error err = {0}; - int32_t result = weaveffi_calculator_mul(a, b, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_int32(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_calculator_div(napi_env env, napi_callback_info info) { - size_t argc = 2; - napi_value args[2]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int32_t a; - napi_get_value_int32(env, args[0], &a); - int32_t b; - napi_get_value_int32(env, args[1], &b); - weaveffi_error err = {0}; - int32_t result = weaveffi_calculator_div(a, b, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_int32(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_calculator_echo(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - size_t s_len; - napi_get_value_string_utf8(env, args[0], NULL, 0, &s_len); - char* s = (char*)malloc(s_len + 1); - napi_get_value_string_utf8(env, args[0], s, s_len + 1, &s_len); - weaveffi_error err = {0}; - const char* result = weaveffi_calculator_echo(s, &err); - free(s); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret); - weaveffi_free_string(result); - return ret; -} - -static napi_value Init(napi_env env, napi_value exports) { - napi_property_descriptor props[] = { - { "add", NULL, Napi_weaveffi_calculator_add, NULL, NULL, NULL, napi_default, NULL }, - { "mul", NULL, Napi_weaveffi_calculator_mul, NULL, NULL, NULL, napi_default, NULL }, - { "div", NULL, Napi_weaveffi_calculator_div, NULL, NULL, NULL, napi_default, NULL }, - { "echo", NULL, Napi_weaveffi_calculator_echo, NULL, NULL, NULL, napi_default, NULL }, - }; - napi_define_properties(env, exports, 4, props); - return exports; -} - -NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) - -// END weaveffi_addon.c diff --git a/crates/weaveffi-cli/tests/snapshots/node_contacts__binding_gyp.snap b/crates/weaveffi-cli/tests/snapshots/node_contacts__binding_gyp.snap deleted file mode 100644 index 3997b5bb..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_contacts__binding_gyp.snap +++ /dev/null @@ -1,19 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -{ - "targets": [ - { - "target_name": "weaveffi", - "sources": ["weaveffi_addon.c"], - "include_dirs": ["../c"], - "libraries": ["-lweaveffi"] - } - ] -} - -# END binding.gyp diff --git a/crates/weaveffi-cli/tests/snapshots/node_contacts__index_js.snap b/crates/weaveffi-cli/tests/snapshots/node_contacts__index_js.snap deleted file mode 100644 index b4916245..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_contacts__index_js.snap +++ /dev/null @@ -1,272 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// The WEAVEFFI_ADDON environment variable overrides the addon location -// (an absolute path to the built .node file); otherwise prefer the -// default node-gyp output path and fall back to a prebuilt index.node -// placed next to this file. -let addon; -if (process.env.WEAVEFFI_ADDON) { - addon = require(process.env.WEAVEFFI_ADDON); -} else { - try { - addon = require('./build/Release/weaveffi.node'); - } catch (e) { - addon = require('./index.node'); - } -} - -// Re-export every native binding, then layer the idiomatic wrappers -// (error classes, interface classes, buffer pack/unpack, function -// wrappers) on top. -const wv = {}; -for (const _name of Object.getOwnPropertyNames(addon)) { - wv[_name] = addon[_name]; -} - -class WeaveFFIError extends Error { - constructor(code, message) { - super('(' + code + ') ' + (message || '')); - this.name = 'WeaveFFIError'; - this.code = code; - this.errorMessage = message || ''; - } -} -wv.WeaveFFIError = WeaveFFIError; -function __generic(code, message) { - return new WeaveFFIError(code, message); -} -function __rebrand(e, map) { - return e && typeof e.code === 'number' ? map(e.code, e.message, e.payload) : e; -} -function __invoke(fn, args, map) { - try { - return fn.apply(null, args); - } catch (e) { - throw __rebrand(e, map); - } -} -function __invokeAsync(fn, args, map) { - return fn.apply(null, args).catch((e) => { - throw __rebrand(e, map); - }); -} - -// --- Private value-buffer runtime (WeaveFFI wire format) -------------------- -// Little-endian, packed, no alignment. Decoders reject truncated buffers, -// invalid bool/flag bytes, hostile length prefixes, and trailing bytes. -const __utf8 = new TextDecoder('utf-8', { fatal: true }); -function __bufferError(what) { - return new WeaveFFIError(-2, 'malformed value buffer: ' + what); -} -class __Writer { - constructor() { - this._buf = Buffer.alloc(256); - this._len = 0; - } - _reserve(n) { - if (this._len + n <= this._buf.length) return; - let cap = this._buf.length; - while (cap < this._len + n) cap *= 2; - const grown = Buffer.alloc(cap); - this._buf.copy(grown, 0, 0, this._len); - this._buf = grown; - } - bool(v) { this._reserve(1); this._buf[this._len++] = v ? 1 : 0; } - i8(v) { this._reserve(1); this._buf.writeInt8(v, this._len); this._len += 1; } - u8(v) { this._reserve(1); this._buf.writeUInt8(v, this._len); this._len += 1; } - i16(v) { this._reserve(2); this._buf.writeInt16LE(v, this._len); this._len += 2; } - u16(v) { this._reserve(2); this._buf.writeUInt16LE(v, this._len); this._len += 2; } - i32(v) { this._reserve(4); this._buf.writeInt32LE(v, this._len); this._len += 4; } - u32(v) { this._reserve(4); this._buf.writeUInt32LE(v, this._len); this._len += 4; } - i64(v) { this._reserve(8); this._buf.writeBigInt64LE(BigInt(v), this._len); this._len += 8; } - u64(v) { this._reserve(8); this._buf.writeBigUInt64LE(BigInt(v), this._len); this._len += 8; } - f32(v) { this._reserve(4); this._buf.writeFloatLE(v, this._len); this._len += 4; } - f64(v) { this._reserve(8); this._buf.writeDoubleLE(v, this._len); this._len += 8; } - str(v) { - const b = Buffer.from(String(v), 'utf8'); - this.u32(b.length); - this._reserve(b.length); - b.copy(this._buf, this._len); - this._len += b.length; - } - bytes(v) { - this.u32(v.length); - this._reserve(v.length); - this._buf.set(v, this._len); - this._len += v.length; - } - finish() { return this._buf.subarray(0, this._len); } -} -class __Reader { - constructor(buf) { - this._buf = Buffer.isBuffer(buf) ? buf : Buffer.from(buf); - this._pos = 0; - } - _take(n, what) { - if (this._pos + n > this._buf.length) throw __bufferError(what); - const at = this._pos; - this._pos += n; - return at; - } - bool() { - const b = this._buf[this._take(1, 'bool')]; - if (b > 1) throw __bufferError('bool byte out of range'); - return b === 1; - } - i8() { return this._buf.readInt8(this._take(1, 'i8')); } - u8() { return this._buf.readUInt8(this._take(1, 'u8')); } - i16() { return this._buf.readInt16LE(this._take(2, 'i16')); } - u16() { return this._buf.readUInt16LE(this._take(2, 'u16')); } - i32() { return this._buf.readInt32LE(this._take(4, 'i32')); } - u32() { return this._buf.readUInt32LE(this._take(4, 'u32')); } - i64() { return this._buf.readBigInt64LE(this._take(8, 'i64')); } - u64() { return this._buf.readBigUInt64LE(this._take(8, 'u64')); } - f32() { return this._buf.readFloatLE(this._take(4, 'f32')); } - f64() { return this._buf.readDoubleLE(this._take(8, 'f64')); } - len() { - const n = this.u32(); - if (n > this._buf.length - this._pos) throw __bufferError('length prefix exceeds remaining bytes'); - return n; - } - str() { - const n = this.len(); - const at = this._take(n, 'string bytes'); - try { - return __utf8.decode(this._buf.subarray(at, at + n)); - } catch (e) { - throw __bufferError('string is not valid UTF-8'); - } - } - bytes() { - const n = this.len(); - const at = this._take(n, 'byte buffer'); - return Buffer.from(this._buf.subarray(at, at + n)); - } - end() { - if (this._pos !== this._buf.length) throw __bufferError('trailing bytes after value'); - } -} -function __encode(f, v) { const w = new __Writer(); f(w, v); return w.finish(); } -function __decode(f, b) { const r = new __Reader(b); const v = f(r); r.end(); return v; } -function __wOpt(w, v, f) { if (v === null || v === undefined) { w.bool(false); } else { w.bool(true); f(w, v); } } -function __rOpt(r, f) { return r.bool() ? f(r) : null; } -function __wList(w, v, f) { w.u32(v.length); for (const e of v) f(w, e); } -function __rList(r, f) { - const n = r.len(); - const out = []; - for (let i = 0; i < n; i++) out.push(f(r)); - return out; -} -function __wMap(w, v, kf, vf) { - const keys = Object.keys(v); - w.u32(keys.length); - for (const k of keys) { kf(w, k); vf(w, v[k]); } -} -function __rMap(r, kf, vf) { - const n = r.len(); - const out = {}; - for (let i = 0; i < n; i++) { - const k = kf(r); - out[k] = vf(r); - } - return out; -} - -function __packContact(w, v) { - w.i64(v.id); - w.str(v.first_name); - w.str(v.last_name); - __wOpt(w, v.email, (w, v) => w.str(v)); - w.i32(v.contact_type); -} -function __unpackContact(r) { - return { - id: Number(r.i64()), - first_name: r.str(), - last_name: r.str(), - email: __rOpt(r, (r) => r.str()), - contact_type: r.i32(), - }; -} - -class ContactsError extends WeaveFFIError { - constructor(code, message) { - super(code, message); - this.name = 'ContactsError'; - } -} -wv.ContactsError = ContactsError; -class InvalidNameError extends ContactsError { - constructor(message) { - super(1, message || 'name must not be empty'); - this.name = 'InvalidNameError'; - } -} -InvalidNameError.CODE = 1; -wv.InvalidNameError = InvalidNameError; -class NotFoundError extends ContactsError { - constructor(message) { - super(2, message || 'contact not found'); - this.name = 'NotFoundError'; - } -} -NotFoundError.CODE = 2; -wv.NotFoundError = NotFoundError; -const __contactsErrorCodes = Object.freeze({ 1: InvalidNameError, 2: NotFoundError }); -function __contactsErrorFrom(code, message, payload) { - const _cls = __contactsErrorCodes[code]; - const _err = _cls === undefined ? new WeaveFFIError(code, message) : new _cls(message); - return _err; -} - -class ContactBook { - constructor() { - this._handle = __invoke(addon.ContactBook_new, [], __generic); - ContactBook._cleanup.register(this, this._handle, this); - } - add(firstName, lastName, email, contactType) { - const _r = __invoke(addon.ContactBook_add, [this._handle, firstName, lastName, __encode((w, v) => __wOpt(w, v, (w, v) => w.str(v)), email), contactType], __contactsErrorFrom); - return __decode(__unpackContact, _r); - } - get(id) { - const _r = __invoke(addon.ContactBook_get, [this._handle, id], __contactsErrorFrom); - return __decode(__unpackContact, _r); - } - list() { - const _r = __invoke(addon.ContactBook_list, [this._handle], __generic); - return __decode((r) => __rList(r, __unpackContact), _r); - } - remove(id) { - return __invoke(addon.ContactBook_remove, [this._handle, id], __generic); - } - count() { - return __invoke(addon.ContactBook_count, [this._handle], __generic); - } - destroy() { - if (this._handle) { - ContactBook._cleanup.unregister(this); - addon.ContactBook_destroy(this._handle); - this._handle = 0; - } - } -} -ContactBook._fromHandle = function (handle) { - const _o = Object.create(ContactBook.prototype); - _o._handle = handle; - ContactBook._cleanup.register(_o, handle, _o); - return _o; -}; -ContactBook._cleanup = new FinalizationRegistry((handle) => { - if (handle) { addon.ContactBook_destroy(handle); } -}); -wv.ContactBook = ContactBook; - - -module.exports = wv; - -// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_contacts__package_json.snap b/crates/weaveffi-cli/tests/snapshots/node_contacts__package_json.snap deleted file mode 100644 index 6fd366d2..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_contacts__package_json.snap +++ /dev/null @@ -1,17 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -{ - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", - "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", - "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", - "main": "index.js", - "types": "types.d.ts", - "gypfile": true, - "scripts": { - "install": "node-gyp rebuild" - } -} diff --git a/crates/weaveffi-cli/tests/snapshots/node_contacts__types_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_contacts__types_d_ts.snap deleted file mode 100644 index 7f610699..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_contacts__types_d_ts.snap +++ /dev/null @@ -1,78 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// Generated types for WeaveFFI functions -/** - * Base class of every error thrown by these bindings. Non-throwing - * functions reject or throw it directly for panics and marshalling - * failures; throwing functions surface a module domain subclass. - */ -export class WeaveFFIError extends Error { - /** The numeric ABI error code. */ - code: number; - /** The raw producer message, without the code prefix. */ - errorMessage: string; - constructor(code: number, message: string); -} -/** Typed errors reported by the `contacts` module's throwing functions. */ -export class ContactsError extends WeaveFFIError { - constructor(code: number, message: string); -} -export class InvalidNameError extends ContactsError { - static readonly CODE: 1; - constructor(message?: string); -} -export class NotFoundError extends ContactsError { - static readonly CODE: 2; - constructor(message?: string); -} -export interface Contact { - id: number; - first_name: string; - last_name: string; - email: string | null; - contact_type: ContactType; -} -export enum ContactType { - Personal = 0, - Work = 1, - Other = 2, -} -// module contacts -/** An in-memory address book owning its contacts */ -export class ContactBook { - /** - * Maps to C function: weaveffi_contacts_ContactBook_new - */ - constructor(); - /** - * Maps to C function: weaveffi_contacts_ContactBook_add - * @throws {ContactsError} - */ - add(firstName: string, lastName: string, email: string | null, contactType: ContactType): Contact; - /** - * Maps to C function: weaveffi_contacts_ContactBook_get - * @throws {ContactsError} - */ - get(id: number): Contact; - /** - * Maps to C function: weaveffi_contacts_ContactBook_list - */ - list(): Contact[]; - /** - * Maps to C function: weaveffi_contacts_ContactBook_remove - */ - remove(id: number): boolean; - /** - * Maps to C function: weaveffi_contacts_ContactBook_count - */ - count(): number; - /** Free the underlying native object. */ - destroy(): void; -} - -// END types.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/node_contacts__weaveffi_addon_c.snap b/crates/weaveffi-cli/tests/snapshots/node_contacts__weaveffi_addon_c.snap deleted file mode 100644 index 9b60d3d8..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_contacts__weaveffi_addon_c.snap +++ /dev/null @@ -1,188 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include -#include "weaveffi.h" -#include -#include - -static napi_value weaveffi_napi_error_value(napi_env env, int32_t code, const char* message, const uint8_t* payload_ptr, size_t payload_len) { - napi_value msg; - napi_create_string_utf8(env, message ? message : "", NAPI_AUTO_LENGTH, &msg); - napi_value err; - napi_create_error(env, NULL, msg, &err); - napi_value code_val; - napi_create_int32(env, code, &code_val); - napi_set_named_property(env, err, "code", code_val); - if (payload_ptr != NULL) { - napi_value payload_val; - napi_create_buffer_copy(env, payload_len, payload_ptr, NULL, &payload_val); - napi_set_named_property(env, err, "payload", payload_val); - } - return err; -} - -static napi_value Napi_weaveffi_contacts_ContactBook_new(napi_env env, napi_callback_info info) { - size_t argc = 0; - napi_get_cb_info(env, info, &argc, NULL, NULL, NULL); - weaveffi_error err = {0}; - weaveffi_contacts_ContactBook* result = weaveffi_contacts_ContactBook_new(&err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_int64(env, (int64_t)(intptr_t)result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_contacts_ContactBook_add(napi_env env, napi_callback_info info) { - size_t argc = 5; - napi_value args[5]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - size_t first_name_len; - napi_get_value_string_utf8(env, args[1], NULL, 0, &first_name_len); - char* first_name = (char*)malloc(first_name_len + 1); - napi_get_value_string_utf8(env, args[1], first_name, first_name_len + 1, &first_name_len); - size_t last_name_len; - napi_get_value_string_utf8(env, args[2], NULL, 0, &last_name_len); - char* last_name = (char*)malloc(last_name_len + 1); - napi_get_value_string_utf8(env, args[2], last_name, last_name_len + 1, &last_name_len); - void* email_raw; - size_t email_len; - napi_get_buffer_info(env, args[3], &email_raw, &email_len); - int32_t contact_type; - napi_get_value_int32(env, args[4], &contact_type); - weaveffi_error err = {0}; - size_t out_len; - const uint8_t* result = weaveffi_contacts_ContactBook_add((const weaveffi_contacts_ContactBook*)(intptr_t)self_raw, first_name, last_name, (const uint8_t*)email_raw, email_len, (weaveffi_contacts_ContactType)contact_type, &out_len, &err); - free(first_name); - free(last_name); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_buffer_copy(env, out_len, result, NULL, &ret); - weaveffi_free_bytes((uint8_t*)result, out_len); - return ret; -} - -static napi_value Napi_weaveffi_contacts_ContactBook_get(napi_env env, napi_callback_info info) { - size_t argc = 2; - napi_value args[2]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - int64_t id; - napi_get_value_int64(env, args[1], &id); - weaveffi_error err = {0}; - size_t out_len; - const uint8_t* result = weaveffi_contacts_ContactBook_get((const weaveffi_contacts_ContactBook*)(intptr_t)self_raw, id, &out_len, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_buffer_copy(env, out_len, result, NULL, &ret); - weaveffi_free_bytes((uint8_t*)result, out_len); - return ret; -} - -static napi_value Napi_weaveffi_contacts_ContactBook_list(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - weaveffi_error err = {0}; - size_t out_len; - const uint8_t* result = weaveffi_contacts_ContactBook_list((const weaveffi_contacts_ContactBook*)(intptr_t)self_raw, &out_len, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_buffer_copy(env, out_len, result, NULL, &ret); - weaveffi_free_bytes((uint8_t*)result, out_len); - return ret; -} - -static napi_value Napi_weaveffi_contacts_ContactBook_remove(napi_env env, napi_callback_info info) { - size_t argc = 2; - napi_value args[2]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - int64_t id; - napi_get_value_int64(env, args[1], &id); - weaveffi_error err = {0}; - bool result = weaveffi_contacts_ContactBook_remove((const weaveffi_contacts_ContactBook*)(intptr_t)self_raw, id, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_get_boolean(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_contacts_ContactBook_count(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - weaveffi_error err = {0}; - int32_t result = weaveffi_contacts_ContactBook_count((const weaveffi_contacts_ContactBook*)(intptr_t)self_raw, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_int32(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_contacts_ContactBook_destroy(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - weaveffi_contacts_ContactBook* self = (weaveffi_contacts_ContactBook*)(intptr_t)self_raw; - weaveffi_contacts_ContactBook_destroy(self); - napi_value ret; - napi_get_undefined(env, &ret); - return ret; -} - -static napi_value Init(napi_env env, napi_value exports) { - napi_property_descriptor props[] = { - { "ContactBook_new", NULL, Napi_weaveffi_contacts_ContactBook_new, NULL, NULL, NULL, napi_default, NULL }, - { "ContactBook_add", NULL, Napi_weaveffi_contacts_ContactBook_add, NULL, NULL, NULL, napi_default, NULL }, - { "ContactBook_get", NULL, Napi_weaveffi_contacts_ContactBook_get, NULL, NULL, NULL, napi_default, NULL }, - { "ContactBook_list", NULL, Napi_weaveffi_contacts_ContactBook_list, NULL, NULL, NULL, napi_default, NULL }, - { "ContactBook_remove", NULL, Napi_weaveffi_contacts_ContactBook_remove, NULL, NULL, NULL, napi_default, NULL }, - { "ContactBook_count", NULL, Napi_weaveffi_contacts_ContactBook_count, NULL, NULL, NULL, napi_default, NULL }, - { "ContactBook_destroy", NULL, Napi_weaveffi_contacts_ContactBook_destroy, NULL, NULL, NULL, napi_default, NULL }, - }; - napi_define_properties(env, exports, 7, props); - return exports; -} - -NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) - -// END weaveffi_addon.c diff --git a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__weaveffi_addon_c.snap b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__weaveffi_addon_c.snap index 0aa8984d..04fd6d34 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__weaveffi_addon_c.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_docs_everywhere__weaveffi_addon_c.snap @@ -7,6 +7,7 @@ source: crates/weaveffi-cli/tests/snapshots.rs #include #include "weaveffi.h" +#include #include #include @@ -314,6 +315,15 @@ static napi_value Napi_weaveffi_docs_publish_async(napi_env env, napi_callback_i } static napi_value Init(napi_env env, napi_value exports) { + { + uint32_t found = weaveffi_abi_version(); + if (found != WEAVEFFI_ABI_VERSION) { + char msg[160]; + snprintf(msg, sizeof msg, "WeaveFFI ABI mismatch: these bindings expect revision %u but the loaded library reports revision %u", (unsigned)WEAVEFFI_ABI_VERSION, (unsigned)found); + napi_throw_error(env, NULL, msg); + return NULL; + } + } napi_property_descriptor props[] = { { "Notebook_new", NULL, Napi_weaveffi_docs_Notebook_new, NULL, NULL, NULL, napi_default, NULL }, { "Notebook_max_documents", NULL, Napi_weaveffi_docs_Notebook_max_documents, NULL, NULL, NULL, napi_default, NULL }, diff --git a/crates/weaveffi-cli/tests/snapshots/node_async_demo__binding_gyp.snap b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__binding_gyp.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/node_async_demo__binding_gyp.snap rename to crates/weaveffi-cli/tests/snapshots/node_edge_cases__binding_gyp.snap 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 new file mode 100644 index 00000000..489184cc --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__index_js.snap @@ -0,0 +1,559 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from weaveffi.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate weaveffi.yml -o + +// The WEAVEFFI_ADDON environment variable overrides the addon location +// (an absolute path to the built .node file); otherwise prefer the +// default node-gyp output path and fall back to a prebuilt index.node +// placed next to this file. +let addon; +if (process.env.WEAVEFFI_ADDON) { + addon = require(process.env.WEAVEFFI_ADDON); +} else { + try { + addon = require('./build/Release/weaveffi.node'); + } catch (e) { + addon = require('./index.node'); + } +} + +// Re-export every native binding, then layer the idiomatic wrappers +// (error classes, interface classes, buffer pack/unpack, function +// wrappers) on top. +const wv = {}; +for (const _name of Object.getOwnPropertyNames(addon)) { + wv[_name] = addon[_name]; +} + +class WeaveFFIError extends Error { + constructor(code, message) { + super('(' + code + ') ' + (message || '')); + this.name = 'WeaveFFIError'; + this.code = code; + this.errorMessage = message || ''; + } +} +wv.WeaveFFIError = WeaveFFIError; +function __generic(code, message) { + return new WeaveFFIError(code, message); +} +function __rebrand(e, map) { + return e && typeof e.code === 'number' ? map(e.code, e.message, e.payload) : e; +} +function __invoke(fn, args, map) { + try { + return fn.apply(null, args); + } catch (e) { + throw __rebrand(e, map); + } +} +function __invokeAsync(fn, args, map) { + return fn.apply(null, args).catch((e) => { + throw __rebrand(e, map); + }); +} + +// --- Private value-buffer runtime (WeaveFFI wire format) -------------------- +// Little-endian, packed, no alignment. Decoders reject truncated buffers, +// invalid bool/flag bytes, hostile length prefixes, and trailing bytes. +const __utf8 = new TextDecoder('utf-8', { fatal: true }); +function __bufferError(what) { + return new WeaveFFIError(-2, 'malformed value buffer: ' + what); +} +class __Writer { + constructor() { + this._buf = Buffer.alloc(256); + this._len = 0; + } + _reserve(n) { + if (this._len + n <= this._buf.length) return; + let cap = this._buf.length; + while (cap < this._len + n) cap *= 2; + const grown = Buffer.alloc(cap); + this._buf.copy(grown, 0, 0, this._len); + this._buf = grown; + } + bool(v) { this._reserve(1); this._buf[this._len++] = v ? 1 : 0; } + i8(v) { this._reserve(1); this._buf.writeInt8(v, this._len); this._len += 1; } + u8(v) { this._reserve(1); this._buf.writeUInt8(v, this._len); this._len += 1; } + i16(v) { this._reserve(2); this._buf.writeInt16LE(v, this._len); this._len += 2; } + u16(v) { this._reserve(2); this._buf.writeUInt16LE(v, this._len); this._len += 2; } + i32(v) { this._reserve(4); this._buf.writeInt32LE(v, this._len); this._len += 4; } + u32(v) { this._reserve(4); this._buf.writeUInt32LE(v, this._len); this._len += 4; } + i64(v) { this._reserve(8); this._buf.writeBigInt64LE(BigInt(v), this._len); this._len += 8; } + u64(v) { this._reserve(8); this._buf.writeBigUInt64LE(BigInt(v), this._len); this._len += 8; } + f32(v) { this._reserve(4); this._buf.writeFloatLE(v, this._len); this._len += 4; } + f64(v) { this._reserve(8); this._buf.writeDoubleLE(v, this._len); this._len += 8; } + str(v) { + const b = Buffer.from(String(v), 'utf8'); + this.u32(b.length); + this._reserve(b.length); + b.copy(this._buf, this._len); + this._len += b.length; + } + bytes(v) { + this.u32(v.length); + this._reserve(v.length); + this._buf.set(v, this._len); + this._len += v.length; + } + finish() { return this._buf.subarray(0, this._len); } +} +class __Reader { + constructor(buf) { + this._buf = Buffer.isBuffer(buf) ? buf : Buffer.from(buf); + this._pos = 0; + } + _take(n, what) { + if (this._pos + n > this._buf.length) throw __bufferError(what); + const at = this._pos; + this._pos += n; + return at; + } + bool() { + const b = this._buf[this._take(1, 'bool')]; + if (b > 1) throw __bufferError('bool byte out of range'); + return b === 1; + } + i8() { return this._buf.readInt8(this._take(1, 'i8')); } + u8() { return this._buf.readUInt8(this._take(1, 'u8')); } + i16() { return this._buf.readInt16LE(this._take(2, 'i16')); } + u16() { return this._buf.readUInt16LE(this._take(2, 'u16')); } + i32() { return this._buf.readInt32LE(this._take(4, 'i32')); } + u32() { return this._buf.readUInt32LE(this._take(4, 'u32')); } + i64() { return this._buf.readBigInt64LE(this._take(8, 'i64')); } + u64() { return this._buf.readBigUInt64LE(this._take(8, 'u64')); } + f32() { return this._buf.readFloatLE(this._take(4, 'f32')); } + f64() { return this._buf.readDoubleLE(this._take(8, 'f64')); } + len() { + const n = this.u32(); + if (n > this._buf.length - this._pos) throw __bufferError('length prefix exceeds remaining bytes'); + return n; + } + str() { + const n = this.len(); + const at = this._take(n, 'string bytes'); + try { + return __utf8.decode(this._buf.subarray(at, at + n)); + } catch (e) { + throw __bufferError('string is not valid UTF-8'); + } + } + bytes() { + const n = this.len(); + const at = this._take(n, 'byte buffer'); + return Buffer.from(this._buf.subarray(at, at + n)); + } + end() { + if (this._pos !== this._buf.length) throw __bufferError('trailing bytes after value'); + } +} +function __encode(f, v) { const w = new __Writer(); f(w, v); return w.finish(); } +function __decode(f, b) { const r = new __Reader(b); const v = f(r); r.end(); return v; } +function __wOpt(w, v, f) { if (v === null || v === undefined) { w.bool(false); } else { w.bool(true); f(w, v); } } +function __rOpt(r, f) { return r.bool() ? f(r) : null; } +function __wList(w, v, f) { w.u32(v.length); for (const e of v) f(w, e); } +function __rList(r, f) { + const n = r.len(); + const out = []; + for (let i = 0; i < n; i++) out.push(f(r)); + return out; +} +function __wMap(w, v, kf, vf) { + const keys = Object.keys(v); + w.u32(keys.length); + for (const k of keys) { kf(w, k); vf(w, v[k]); } +} +function __rMap(r, kf, vf) { + const n = r.len(); + const out = {}; + for (let i = 0; i < n; i++) { + const k = kf(r); + out[k] = vf(r); + } + return out; +} + +function __packLeaf(w, v) { + w.f32(v.weight); +} +function __unpackLeaf(r) { + return { + weight: r.f32(), + }; +} +function __packInner(w, v) { + __packLeaf(w, v.leaf); + __wList(w, v.leaves, __packLeaf); +} +function __unpackInner(r) { + return { + leaf: __unpackLeaf(r), + leaves: __rList(r, __unpackLeaf), + }; +} +function __packOuter(w, v) { + __packInner(w, v.inner); + __wOpt(w, v.maybe_inner, __packInner); + __wMap(w, v.by_name, (w, k) => w.str(k), __packInner); + __wList(w, v.modes, (w, v) => w.i32(v)); + __wList(w, v.matrix, (w, v) => __wList(w, v, (w, v) => w.i32(v))); + __wList(w, v.sparse, (w, v) => __wOpt(w, v, (w, v) => w.i64(v))); + __wOpt(w, v.blob, (w, v) => w.bytes(v)); + __wOpt(w, v.note, (w, v) => w.str(v)); + __wMap(w, v.by_id, (w, k) => w.i64(k), (w, v) => w.str(v)); + w.bool(v._hidden); + w.u8(v.v2); +} +function __unpackOuter(r) { + return { + inner: __unpackInner(r), + maybe_inner: __rOpt(r, __unpackInner), + by_name: __rMap(r, (r) => r.str(), __unpackInner), + modes: __rList(r, (r) => r.i32()), + matrix: __rList(r, (r) => __rList(r, (r) => r.i32())), + sparse: __rList(r, (r) => __rOpt(r, (r) => Number(r.i64()))), + blob: __rOpt(r, (r) => r.bytes()), + note: __rOpt(r, (r) => r.str()), + by_id: __rMap(r, (r) => Number(r.i64()), (r) => r.str()), + _hidden: r.bool(), + v2: r.u8(), + }; +} +function __packKeywords(w, v) { + w.i32(v.class); + w.i32(v.def); + w.i32(v.import); + w.i32(v.from); + w.i32(v.func); + w.i32(v.range); + w.i32(v.register); + w.i32(v.volatile); + w.i32(v.new); + w.i32(v.default); + w.i32(v.end); + w.i32(v.in); + w.i32(v.is); + w.i32(v.int); + w.i32(v.var); + w.i32(v.val); + w.i32(v.self); + w.i32(v.this); + w.i32(v.object); + w.i32(v.string); + w.i32(v.case); + w.i32(v.select); + w.i32(v.switch); + w.i32(v.void); + w.i32(v.union); + w.i32(v.namespace); + w.i32(v.operator); + w.i32(v.template); + w.i32(v.package); + w.i32(v.protocol); + w.i32(v.guard); + w.i32(v.defer); + w.i32(v.inout); + w.i32(v.internal); + w.i32(v.when); + w.i32(v.fun); + w.i32(v.data); + w.i32(v.late); + w.i32(v.part); + w.i32(v.out); + w.i32(v.ref); + w.i32(v.event); + w.i32(v.base); + w.i32(v.lambda); + w.i32(v.yield); + w.i32(v.nil); + w.i32(v.null); + w.i32(v.begin); + w.i32(v.rescue); + w.i32(v.module); + w.i32(v.chan); + w.i32(v.go); + w.i32(v.let); + w.i32(v.export); + w.i32(v.delete); + w.i32(v.typedef); + w.i32(v.sizeof); + w.i32(v.static); + w.i32(v.super); +} +function __unpackKeywords(r) { + return { + class: r.i32(), + def: r.i32(), + import: r.i32(), + from: r.i32(), + func: r.i32(), + range: r.i32(), + register: r.i32(), + volatile: r.i32(), + new: r.i32(), + default: r.i32(), + end: r.i32(), + in: r.i32(), + is: r.i32(), + int: r.i32(), + var: r.i32(), + val: r.i32(), + self: r.i32(), + this: r.i32(), + object: r.i32(), + string: r.i32(), + case: r.i32(), + select: r.i32(), + switch: r.i32(), + void: r.i32(), + union: r.i32(), + namespace: r.i32(), + operator: r.i32(), + template: r.i32(), + package: r.i32(), + protocol: r.i32(), + guard: r.i32(), + defer: r.i32(), + inout: r.i32(), + internal: r.i32(), + when: r.i32(), + fun: r.i32(), + data: r.i32(), + late: r.i32(), + part: r.i32(), + out: r.i32(), + ref: r.i32(), + event: r.i32(), + base: r.i32(), + lambda: r.i32(), + yield: r.i32(), + nil: r.i32(), + null: r.i32(), + begin: r.i32(), + rescue: r.i32(), + module: r.i32(), + chan: r.i32(), + go: r.i32(), + let: r.i32(), + export: r.i32(), + delete: r.i32(), + typedef: r.i32(), + sizeof: r.i32(), + static: r.i32(), + super: r.i32(), + }; +} +function __packRetired(w, v) { + w.i64(v.id); +} +function __unpackRetired(r) { + return { + id: Number(r.i64()), + }; +} + +// Lazy iterator over a native producer: one native `next` per step. +// The native handle is released on exhaustion, by `return()` on early +// exit, or by the external's finalizer if the iterator is abandoned. +class WeaveFFIIterator { + constructor(ext, nextFn, destroyFn, map, wrapElem) { + this._ext = ext; + this._nextFn = nextFn; + this._destroyFn = destroyFn; + this._map = map; + this._wrapElem = wrapElem; + this._done = false; + } + next() { + if (this._done) { + return { done: true, value: undefined }; + } + const _v = __invoke(this._nextFn, [this._ext], this._map); + if (_v === undefined) { + this._done = true; + return { done: true, value: undefined }; + } + return { done: false, value: this._wrapElem ? this._wrapElem(_v) : _v }; + } + return(value) { + if (!this._done) { + this._done = true; + this._destroyFn(this._ext); + } + return { done: true, value }; + } + [Symbol.iterator]() { + return this; + } +} + +class EdgeErrorsError extends WeaveFFIError { + constructor(code, message) { + super(code, message); + this.name = 'EdgeErrorsError'; + } +} +wv.EdgeErrorsError = EdgeErrorsError; +class BusyError extends EdgeErrorsError { + constructor(message) { + super(7, message || 'Resource is busy'); + this.name = 'BusyError'; + } +} +BusyError.CODE = 7; +wv.BusyError = BusyError; +const __edgeCasesErrorCodes = Object.freeze({ 7: BusyError }); +function __edgeCasesErrorFrom(code, message, payload) { + const _cls = __edgeCasesErrorCodes[code]; + const _err = _cls === undefined ? new WeaveFFIError(code, message) : new _cls(message); + return _err; +} + +class Widget { + refresh() { + return __invoke(addon.Widget_refresh, [this._handle], __edgeCasesErrorFrom); + } + rename(class_, import_) { + return __invoke(addon.Widget_rename, [this._handle, class_, __encode((w, v) => __wOpt(w, v, (w, v) => w.str(v)), import_)], __generic); + } + fetch(id) { + return __invokeAsync(addon.Widget_fetch, [this._handle, id], __edgeCasesErrorFrom).then((_r) => __decode(__unpackOuter, _r)); + } + oldName() { + return __invoke(addon.Widget_old_name, [this._handle], __generic); + } + destroy() { + if (this._handle) { + Widget._cleanup.unregister(this); + addon.Widget_destroy(this._handle); + this._handle = 0; + } + } +} +Widget._fromHandle = function (handle) { + const _o = Object.create(Widget.prototype); + _o._handle = handle; + Widget._cleanup.register(_o, handle, _o); + return _o; +}; +Widget._cleanup = new FinalizationRegistry((handle) => { + if (handle) { addon.Widget_destroy(handle); } +}); +wv.Widget = Widget; + +class Vault { + static open(path) { + const _r = __invoke(addon.Vault_open, [path], __edgeCasesErrorFrom); + return Vault._fromHandle(_r); + } + peek() { + const _r = __invoke(addon.Vault_peek, [this._handle], __generic); + return _r == null ? null : Widget._fromHandle(_r); + } + static probe() { + return __invoke(addon.Vault_probe, [], __edgeCasesErrorFrom); + } + destroy() { + if (this._handle) { + Vault._cleanup.unregister(this); + addon.Vault_destroy(this._handle); + this._handle = 0; + } + } +} +Vault._fromHandle = function (handle) { + const _o = Object.create(Vault.prototype); + _o._handle = handle; + Vault._cleanup.register(_o, handle, _o); + return _o; +}; +Vault._cleanup = new FinalizationRegistry((handle) => { + if (handle) { addon.Vault_destroy(handle); } +}); +wv.Vault = Vault; + +wv.registerBatchListener = function (callback) { + return addon.registerBatchListener(function (items, raw, first, mode) { + callback(__decode((r) => __rList(r, __unpackOuter), items), raw, __decode((r) => __rOpt(r, __unpackOuter), first), mode); + }); +}; +wv.noop = function () { + return __invoke(addon.noop, [], __generic); +}; +wv.select = function (class_, from, func, range, type) { + return __invoke(addon.select, [class_, from, __encode((w, v) => __wList(w, v, (w, v) => w.str(v)), func), __encode((w, v) => __wOpt(w, v, (w, v) => w.i32(v)), range), type], __generic); +}; +wv.nestedOptional = function (items) { + const _r = __invoke(addon.nestedOptional, [__encode((w, v) => __wOpt(w, v, (w, v) => __wList(w, v, (w, v) => __wOpt(w, v, __packOuter))), items)], __generic); + return __decode((r) => __rOpt(r, (r) => __rList(r, (r) => __rOpt(r, __unpackOuter))), _r); +}; +wv.optionalParams = function (text, count, outer, tags, blob, mode) { + const _r = __invoke(addon.optionalParams, [__encode((w, v) => __wOpt(w, v, (w, v) => w.str(v)), text), __encode((w, v) => __wOpt(w, v, (w, v) => w.i32(v)), count), __encode((w, v) => __wOpt(w, v, __packOuter), outer), __encode((w, v) => __wOpt(w, v, (w, v) => __wList(w, v, (w, v) => w.str(v))), tags), __encode((w, v) => __wOpt(w, v, (w, v) => w.bytes(v)), blob), __encode((w, v) => __wOpt(w, v, (w, v) => w.i32(v)), mode)], __generic); + return __decode((r) => __rOpt(r, (r) => r.i32()), _r); +}; +wv.mutableParams = function (text, blob) { + return __invoke(addon.mutableParams, [text, blob], __generic); +}; +wv.makeWidget = function (label) { + const _r = __invoke(addon.makeWidget, [label], __generic); + return Widget._fromHandle(_r); +}; +wv.findWidget = function (id) { + const _r = __invoke(addon.findWidget, [id], __edgeCasesErrorFrom); + return _r == null ? null : Widget._fromHandle(_r); +}; +wv.inspectWidget = function (widget, fallback) { + return __invoke(addon.inspectWidget, [widget instanceof Widget ? widget._handle : widget, fallback instanceof Widget ? fallback._handle : fallback], __generic); +}; +wv.typedHandleParam = function (token, raw) { + return __invoke(addon.typedHandleParam, [token, raw], __generic); +}; +wv.stringStream = function () { + const _it = __invoke(addon.stringStream, [], __generic); + return new WeaveFFIIterator(_it, addon.stringStream_iterNext, addon.stringStream_iterDestroy, __generic, null); +}; +wv.scalarStream = function () { + const _it = __invoke(addon.scalarStream, [], __generic); + return new WeaveFFIIterator(_it, addon.scalarStream_iterNext, addon.scalarStream_iterDestroy, __generic, null); +}; +wv.modeStream = function () { + const _it = __invoke(addon.modeStream, [], __generic); + return new WeaveFFIIterator(_it, addon.modeStream_iterNext, addon.modeStream_iterDestroy, __generic, null); +}; +wv.asyncVoid = function () { + return __invokeAsync(addon.asyncVoid, [], __generic); +}; +wv.asyncBytes = function (size) { + return __invokeAsync(addon.asyncBytes, [size], __generic); +}; +wv.asyncStruct = function () { + return __invokeAsync(addon.asyncStruct, [], __edgeCasesErrorFrom).then((_r) => __decode(__unpackOuter, _r)); +}; +wv.asyncList = function () { + return __invokeAsync(addon.asyncList, [], __edgeCasesErrorFrom).then((_r) => __decode((r) => __rList(r, __unpackOuter), _r)); +}; +wv.asyncOptional = function () { + return __invokeAsync(addon.asyncOptional, [], __generic).then((_r) => __decode((r) => __rOpt(r, (r) => Number(r.i64())), _r)); +}; +wv.asyncEnum = function () { + return __invokeAsync(addon.asyncEnum, [], __generic); +}; +wv.asyncKeywords = function (class_, import_) { + return __invokeAsync(addon.asyncKeywords, [class_, import_], __generic); +}; +wv.keywordsRoundtrip = function (value) { + const _r = __invoke(addon.keywordsRoundtrip, [__encode(__packKeywords, value)], __generic); + return __decode(__unpackKeywords, _r); +}; +wv.legacyValue = function (value) { + const _r = __invoke(addon.legacyValue, [value], __generic); + return __decode(__unpackRetired, _r); +}; + +module.exports = wv; + +// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_async_demo__package_json.snap b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__package_json.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/node_async_demo__package_json.snap rename to crates/weaveffi-cli/tests/snapshots/node_edge_cases__package_json.snap diff --git a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__types_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__types_d_ts.snap new file mode 100644 index 00000000..de170b60 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__types_d_ts.snap @@ -0,0 +1,278 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from weaveffi.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate weaveffi.yml -o + +// Generated types for WeaveFFI functions +/** + * Base class of every error thrown by these bindings. Non-throwing + * functions reject or throw it directly for panics and marshalling + * failures; throwing functions surface a module domain subclass. + */ +export class WeaveFFIError extends Error { + /** The numeric ABI error code. */ + code: number; + /** The raw producer message, without the code prefix. */ + errorMessage: string; + constructor(code: number, message: string); +} +/** Typed errors reported by the `edge_cases` module's throwing functions. */ +export class EdgeErrorsError extends WeaveFFIError { + constructor(code: number, message: string); +} +export class BusyError extends EdgeErrorsError { + static readonly CODE: 7; + constructor(message?: string); +} +export interface Leaf { + weight: number; +} +/** Middle layer of a three-deep struct nesting */ +export interface Inner { + leaf: Leaf; + leaves: Leaf[]; +} +export interface Outer { + inner: Inner; + maybe_inner: Inner | null; + by_name: Record; + modes: Mode[]; + matrix: number[][]; + sparse: (number | null)[]; + blob: Buffer | 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 */ +export interface Retired { + id: number; +} +/** Deprecated enum with non-contiguous values and keyword-like variants */ +export enum Legacy { + None = 0, + Default = 5, + Class = 10, +} +export enum Mode { + Fast = 1, + Safe = 2, +} +// module edge_cases +/** Interface with no constructor; instances come from make_widget */ +export class Widget { + /** + * Void method that can fail + * Maps to C function: weaveffi_edge_cases_Widget_refresh + * @throws {EdgeErrorsError} + */ + refresh(): void; + /** + * Maps to C function: weaveffi_edge_cases_Widget_rename + */ + rename(class_: string, import_: string | null): string; + /** + * Async method with a struct result + * Maps to C function: weaveffi_edge_cases_Widget_fetch + * @throws {EdgeErrorsError} + */ + fetch(id: number): Promise; + /** + * Maps to C function: weaveffi_edge_cases_Widget_old_name + * @deprecated Use rename instead + */ + oldName(): string; + /** Free the underlying native object. */ + destroy(): void; +} +/** Deprecated interface with a throwing constructor */ +export class Vault { + /** + * Fails when the vault is busy + * Maps to C function: weaveffi_edge_cases_Vault_open + * @throws {EdgeErrorsError} + */ + static open(path: string): Vault; + /** + * Maps to C function: weaveffi_edge_cases_Vault_peek + */ + peek(): Widget | null; + /** + * Throwing static + * Maps to C function: weaveffi_edge_cases_Vault_probe + * @throws {EdgeErrorsError} + */ + static probe(): boolean; + /** Free the underlying native object. */ + destroy(): void; +} +export function registerBatchListener(callback: (items: Outer[], raw: Buffer, first: Outer | null, mode: Mode) => void): number +export function unregisterBatchListener(id: number): void +export function registerTickListener(callback: () => void): number +export function unregisterTickListener(id: number): void +/** + * No parameters and no result + * Maps to C function: weaveffi_edge_cases_noop + */ +export function noop(): void +/** + * Function named after a keyword whose parameters are all keywords + * Maps to C function: weaveffi_edge_cases_select + */ +export function select(class_: number, from: string, func: string[], range: number | null, type: boolean): number +/** + * Optional list of optional structs, the deepest legal nesting + * Maps to C function: weaveffi_edge_cases_nested_optional + */ +export function nestedOptional(items: (Outer | null)[] | null): (Outer | null)[] | null +/** + * Maps to C function: weaveffi_edge_cases_optional_params + */ +export function optionalParams(text: string | null, count: number | null, outer: Outer | null, tags: string[] | null, blob: Buffer | null, mode: Mode | null): Mode | null +/** + * Write-back parameters + * Maps to C function: weaveffi_edge_cases_mutable_params + */ +export function mutableParams(text: string, blob: Buffer): void +/** + * Factory for an interface with no constructor + * Maps to C function: weaveffi_edge_cases_make_widget + */ +export function makeWidget(label: string): Widget +/** + * Optional interface return + * Maps to C function: weaveffi_edge_cases_find_widget + * @throws {EdgeErrorsError} + */ +export function findWidget(id: number): Widget | null +/** + * Interface parameter, including an optional one + * Maps to C function: weaveffi_edge_cases_inspect_widget + */ +export function inspectWidget(widget: Widget, fallback: Widget | null): string +/** + * Maps to C function: weaveffi_edge_cases_typed_handle_param + */ +export function typedHandleParam(token: Leaf, raw: bigint): Leaf +/** + * Iterator of strings + * Maps to C function: weaveffi_edge_cases_string_stream + */ +export function stringStream(): IterableIterator +/** + * Iterator of scalars + * Maps to C function: weaveffi_edge_cases_scalar_stream + */ +export function scalarStream(): IterableIterator +/** + * Iterator of enums + * Maps to C function: weaveffi_edge_cases_mode_stream + */ +export function modeStream(): IterableIterator +/** + * Async with no result + * Maps to C function: weaveffi_edge_cases_async_void + */ +export function asyncVoid(): Promise +/** + * Maps to C function: weaveffi_edge_cases_async_bytes + */ +export function asyncBytes(size: number): Promise +/** + * Maps to C function: weaveffi_edge_cases_async_struct + * @throws {EdgeErrorsError} + */ +export function asyncStruct(): Promise +/** + * Maps to C function: weaveffi_edge_cases_async_list + * @throws {EdgeErrorsError} + */ +export function asyncList(): Promise +/** + * Maps to C function: weaveffi_edge_cases_async_optional + */ +export function asyncOptional(): Promise +/** + * Maps to C function: weaveffi_edge_cases_async_enum + */ +export function asyncEnum(): Promise +/** + * Maps to C function: weaveffi_edge_cases_async_keywords + */ +export function asyncKeywords(class_: string, import_: number): Promise +/** + * Maps to C function: weaveffi_edge_cases_keywords_roundtrip + */ +export function keywordsRoundtrip(value: Keywords): Keywords +/** + * Maps to C function: weaveffi_edge_cases_legacy_value + * @deprecated Use Mode-based APIs + */ +export function legacyValue(value: Legacy): Retired + +// END types.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/node_edge_cases__weaveffi_addon_c.snap b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__weaveffi_addon_c.snap new file mode 100644 index 00000000..df5863cb --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/node_edge_cases__weaveffi_addon_c.snap @@ -0,0 +1,1364 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from weaveffi.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate weaveffi.yml -o + +#include +#include "weaveffi.h" +#include +#include +#include + +static napi_value weaveffi_napi_error_value(napi_env env, int32_t code, const char* message, const uint8_t* payload_ptr, size_t payload_len) { + napi_value msg; + napi_create_string_utf8(env, message ? message : "", NAPI_AUTO_LENGTH, &msg); + napi_value err; + napi_create_error(env, NULL, msg, &err); + napi_value code_val; + napi_create_int32(env, code, &code_val); + napi_set_named_property(env, err, "code", code_val); + if (payload_ptr != NULL) { + napi_value payload_val; + napi_create_buffer_copy(env, payload_len, payload_ptr, NULL, &payload_val); + napi_set_named_property(env, err, "payload", payload_val); + } + return err; +} + +typedef struct { + void* iter; +} weaveffi_napi_iter_state; + +typedef struct weaveffi_napi_listener_ctx { + napi_threadsafe_function tsfn; + uint64_t id; + struct weaveffi_napi_listener_ctx* next; +} weaveffi_napi_listener_ctx; + +static weaveffi_napi_listener_ctx* weaveffi_napi_listeners = NULL; + +static napi_value Napi_weaveffi_edge_cases_Widget_refresh(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + int64_t self_raw; + napi_get_value_int64(env, args[0], &self_raw); + weaveffi_error err = {0}; + weaveffi_edge_cases_Widget_refresh((const weaveffi_edge_cases_Widget*)(intptr_t)self_raw, &err); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + napi_get_undefined(env, &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_Widget_rename(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value args[3]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + int64_t self_raw; + napi_get_value_int64(env, args[0], &self_raw); + size_t class_len; + napi_get_value_string_utf8(env, args[1], NULL, 0, &class_len); + char* class = (char*)malloc(class_len + 1); + napi_get_value_string_utf8(env, args[1], class, class_len + 1, &class_len); + void* import_raw; + size_t import_len; + napi_get_buffer_info(env, args[2], &import_raw, &import_len); + weaveffi_error err = {0}; + const char* result = weaveffi_edge_cases_Widget_rename((const weaveffi_edge_cases_Widget*)(intptr_t)self_raw, class, (const uint8_t*)import_raw, import_len, &err); + free(class); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret); + weaveffi_free_string(result); + return ret; +} + +typedef struct { + napi_deferred deferred; + napi_threadsafe_function tsfn; + int32_t err_code; + char* err_msg; + uint8_t* err_payload; + size_t err_payload_len; + uint8_t* result; + size_t result_len; +} weaveffi_edge_cases_Widget_fetch_napi_actx; + +static void weaveffi_edge_cases_Widget_fetch_napi_cb(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { + weaveffi_edge_cases_Widget_fetch_napi_actx* ctx = (weaveffi_edge_cases_Widget_fetch_napi_actx*)context; + if (err != NULL && err->code != 0) { + ctx->err_code = err->code; + ctx->err_msg = err->message ? strdup(err->message) : strdup("unknown error"); + ctx->err_payload_len = err->payload_len; + if (err->payload_ptr != NULL && err->payload_len > 0) { ctx->err_payload = (uint8_t*)malloc(err->payload_len); memcpy(ctx->err_payload, err->payload_ptr, err->payload_len); } + } else { + ctx->result_len = result_len; + if (result_ptr != NULL && result_len > 0) { ctx->result = (uint8_t*)malloc(result_len); memcpy(ctx->result, result_ptr, result_len); } + weaveffi_free_bytes((uint8_t*)result_ptr, result_len); + } + weaveffi_error_free(err); + napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); +} + +static void weaveffi_edge_cases_Widget_fetch_napi_settle(napi_env env, napi_value js_cb, void* context, void* data) { + (void)js_cb; + (void)context; + weaveffi_edge_cases_Widget_fetch_napi_actx* ctx = (weaveffi_edge_cases_Widget_fetch_napi_actx*)data; + if (env != NULL) { + if (ctx->err_code != 0) { + napi_value err_obj = weaveffi_napi_error_value(env, ctx->err_code, ctx->err_msg, ctx->err_payload, ctx->err_payload_len); + napi_reject_deferred(env, ctx->deferred, err_obj); + } else { + napi_value val; + napi_create_buffer_copy(env, ctx->result_len, ctx->result ? (const void*)ctx->result : (const void*)"", NULL, &val); + napi_resolve_deferred(env, ctx->deferred, val); + } + } + free(ctx->err_msg); + free(ctx->err_payload); + free(ctx->result); + napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release); + free(ctx); +} + +static napi_value Napi_weaveffi_edge_cases_Widget_fetch(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + int64_t self_raw; + napi_get_value_int64(env, args[0], &self_raw); + int64_t id; + napi_get_value_int64(env, args[1], &id); + weaveffi_edge_cases_Widget_fetch_napi_actx* ctx = (weaveffi_edge_cases_Widget_fetch_napi_actx*)calloc(1, sizeof(weaveffi_edge_cases_Widget_fetch_napi_actx)); + napi_value promise; + napi_create_promise(env, &ctx->deferred, &promise); + napi_value resource_name; + napi_create_string_utf8(env, "weaveffi_edge_cases_Widget_fetch", NAPI_AUTO_LENGTH, &resource_name); + napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_edge_cases_Widget_fetch_napi_settle, &ctx->tsfn); + weaveffi_edge_cases_Widget_fetch_async((const weaveffi_edge_cases_Widget*)(intptr_t)self_raw, id, weaveffi_edge_cases_Widget_fetch_napi_cb, ctx); + return promise; +} + +static napi_value Napi_weaveffi_edge_cases_Widget_old_name(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + int64_t self_raw; + napi_get_value_int64(env, args[0], &self_raw); + weaveffi_error err = {0}; + const char* result = weaveffi_edge_cases_Widget_old_name((const weaveffi_edge_cases_Widget*)(intptr_t)self_raw, &err); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret); + weaveffi_free_string(result); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_Widget_destroy(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + int64_t self_raw; + napi_get_value_int64(env, args[0], &self_raw); + weaveffi_edge_cases_Widget* self = (weaveffi_edge_cases_Widget*)(intptr_t)self_raw; + weaveffi_edge_cases_Widget_destroy(self); + napi_value ret; + napi_get_undefined(env, &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_Vault_open(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + size_t path_len; + napi_get_value_string_utf8(env, args[0], NULL, 0, &path_len); + char* path = (char*)malloc(path_len + 1); + napi_get_value_string_utf8(env, args[0], path, path_len + 1, &path_len); + weaveffi_error err = {0}; + weaveffi_edge_cases_Vault* result = weaveffi_edge_cases_Vault_open(path, &err); + free(path); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + napi_create_int64(env, (int64_t)(intptr_t)result, &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_Vault_probe(napi_env env, napi_callback_info info) { + size_t argc = 0; + napi_get_cb_info(env, info, &argc, NULL, NULL, NULL); + weaveffi_error err = {0}; + bool result = weaveffi_edge_cases_Vault_probe(&err); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + napi_get_boolean(env, result, &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_Vault_peek(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + int64_t self_raw; + napi_get_value_int64(env, args[0], &self_raw); + weaveffi_error err = {0}; + weaveffi_edge_cases_Widget* result = weaveffi_edge_cases_Vault_peek((const weaveffi_edge_cases_Vault*)(intptr_t)self_raw, &err); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + if (result == NULL) { + napi_get_null(env, &ret); + } else { + napi_create_int64(env, (int64_t)(intptr_t)result, &ret); + } + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_Vault_destroy(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + int64_t self_raw; + napi_get_value_int64(env, args[0], &self_raw); + weaveffi_edge_cases_Vault* self = (weaveffi_edge_cases_Vault*)(intptr_t)self_raw; + weaveffi_edge_cases_Vault_destroy(self); + napi_value ret; + napi_get_undefined(env, &ret); + return ret; +} + +typedef struct { + uint8_t* items_ptr; + size_t items_len; + uint8_t* raw_ptr; + size_t raw_len; + uint8_t* first_ptr; + size_t first_len; + weaveffi_edge_cases_Mode mode; +} weaveffi_edge_cases_OnBatch_fn_payload; + +static void weaveffi_edge_cases_OnBatch_fn_napi_tramp(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, weaveffi_edge_cases_Mode mode, void* context) { + weaveffi_napi_listener_ctx* ctx = (weaveffi_napi_listener_ctx*)context; + weaveffi_edge_cases_OnBatch_fn_payload* p = (weaveffi_edge_cases_OnBatch_fn_payload*)calloc(1, sizeof(weaveffi_edge_cases_OnBatch_fn_payload)); + p->items_len = items_len; + if (items_ptr != NULL && items_len > 0) { p->items_ptr = (uint8_t*)malloc(items_len); memcpy(p->items_ptr, items_ptr, items_len); } + p->raw_len = raw_len; + if (raw_ptr != NULL && raw_len > 0) { p->raw_ptr = (uint8_t*)malloc(raw_len); memcpy(p->raw_ptr, raw_ptr, raw_len); } + p->first_len = first_len; + if (first_ptr != NULL && first_len > 0) { p->first_ptr = (uint8_t*)malloc(first_len); memcpy(p->first_ptr, first_ptr, first_len); } + p->mode = mode; + napi_call_threadsafe_function(ctx->tsfn, p, napi_tsfn_nonblocking); +} + +static void weaveffi_edge_cases_OnBatch_fn_napi_calljs(napi_env env, napi_value js_cb, void* context, void* data) { + (void)context; + weaveffi_edge_cases_OnBatch_fn_payload* p = (weaveffi_edge_cases_OnBatch_fn_payload*)data; + if (env != NULL) { + napi_value undefined; + napi_get_undefined(env, &undefined); + napi_value argv[4]; + napi_create_buffer_copy(env, p->items_len, p->items_ptr ? (const void*)p->items_ptr : (const void*)"", NULL, &argv[0]); + napi_create_buffer_copy(env, p->raw_len, p->raw_ptr ? (const void*)p->raw_ptr : (const void*)"", NULL, &argv[1]); + napi_create_buffer_copy(env, p->first_len, p->first_ptr ? (const void*)p->first_ptr : (const void*)"", NULL, &argv[2]); + napi_create_int32(env, (int32_t)p->mode, &argv[3]); + napi_call_function(env, undefined, js_cb, 4, argv, NULL); + } + free(p->items_ptr); + free(p->raw_ptr); + free(p->first_ptr); + free(p); +} + +typedef struct { +} weaveffi_edge_cases_OnTick_fn_payload; + +static void weaveffi_edge_cases_OnTick_fn_napi_tramp(void* context) { + weaveffi_napi_listener_ctx* ctx = (weaveffi_napi_listener_ctx*)context; + weaveffi_edge_cases_OnTick_fn_payload* p = (weaveffi_edge_cases_OnTick_fn_payload*)calloc(1, sizeof(weaveffi_edge_cases_OnTick_fn_payload)); + napi_call_threadsafe_function(ctx->tsfn, p, napi_tsfn_nonblocking); +} + +static void weaveffi_edge_cases_OnTick_fn_napi_calljs(napi_env env, napi_value js_cb, void* context, void* data) { + (void)context; + weaveffi_edge_cases_OnTick_fn_payload* p = (weaveffi_edge_cases_OnTick_fn_payload*)data; + if (env != NULL) { + napi_value undefined; + napi_get_undefined(env, &undefined); + napi_call_function(env, undefined, js_cb, 0, NULL, NULL); + } + free(p); +} + +static napi_value Napi_weaveffi_edge_cases_register_batch_listener(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + weaveffi_napi_listener_ctx* ctx = (weaveffi_napi_listener_ctx*)calloc(1, sizeof(weaveffi_napi_listener_ctx)); + napi_value resource_name; + napi_create_string_utf8(env, "weaveffi_edge_cases_register_batch_listener", NAPI_AUTO_LENGTH, &resource_name); + napi_create_threadsafe_function(env, args[0], NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_edge_cases_OnBatch_fn_napi_calljs, &ctx->tsfn); + napi_unref_threadsafe_function(env, ctx->tsfn); + uint64_t id = weaveffi_edge_cases_register_batch_listener(weaveffi_edge_cases_OnBatch_fn_napi_tramp, ctx); + ctx->id = id; + ctx->next = weaveffi_napi_listeners; + weaveffi_napi_listeners = ctx; + napi_value ret; + napi_create_double(env, (double)id, &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_unregister_batch_listener(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + double id_d = 0; + napi_get_value_double(env, args[0], &id_d); + uint64_t id = (uint64_t)id_d; + weaveffi_edge_cases_unregister_batch_listener(id); + weaveffi_napi_listener_ctx** link = &weaveffi_napi_listeners; + while (*link != NULL) { + if ((*link)->id == id) { + weaveffi_napi_listener_ctx* found = *link; + *link = found->next; + napi_release_threadsafe_function(found->tsfn, napi_tsfn_release); + free(found); + break; + } + link = &(*link)->next; + } + napi_value ret; + napi_get_undefined(env, &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_register_tick_listener(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + weaveffi_napi_listener_ctx* ctx = (weaveffi_napi_listener_ctx*)calloc(1, sizeof(weaveffi_napi_listener_ctx)); + napi_value resource_name; + napi_create_string_utf8(env, "weaveffi_edge_cases_register_tick_listener", NAPI_AUTO_LENGTH, &resource_name); + napi_create_threadsafe_function(env, args[0], NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_edge_cases_OnTick_fn_napi_calljs, &ctx->tsfn); + napi_unref_threadsafe_function(env, ctx->tsfn); + uint64_t id = weaveffi_edge_cases_register_tick_listener(weaveffi_edge_cases_OnTick_fn_napi_tramp, ctx); + ctx->id = id; + ctx->next = weaveffi_napi_listeners; + weaveffi_napi_listeners = ctx; + napi_value ret; + napi_create_double(env, (double)id, &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_unregister_tick_listener(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + double id_d = 0; + napi_get_value_double(env, args[0], &id_d); + uint64_t id = (uint64_t)id_d; + weaveffi_edge_cases_unregister_tick_listener(id); + weaveffi_napi_listener_ctx** link = &weaveffi_napi_listeners; + while (*link != NULL) { + if ((*link)->id == id) { + weaveffi_napi_listener_ctx* found = *link; + *link = found->next; + napi_release_threadsafe_function(found->tsfn, napi_tsfn_release); + free(found); + break; + } + link = &(*link)->next; + } + napi_value ret; + napi_get_undefined(env, &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_noop(napi_env env, napi_callback_info info) { + size_t argc = 0; + napi_get_cb_info(env, info, &argc, NULL, NULL, NULL); + weaveffi_error err = {0}; + weaveffi_edge_cases_noop(&err); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + napi_get_undefined(env, &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_select(napi_env env, napi_callback_info info) { + size_t argc = 5; + napi_value args[5]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + int32_t class; + napi_get_value_int32(env, args[0], &class); + size_t from_len; + napi_get_value_string_utf8(env, args[1], NULL, 0, &from_len); + char* from = (char*)malloc(from_len + 1); + napi_get_value_string_utf8(env, args[1], from, from_len + 1, &from_len); + void* func_raw; + size_t func_len; + napi_get_buffer_info(env, args[2], &func_raw, &func_len); + void* range_raw; + size_t range_len; + napi_get_buffer_info(env, args[3], &range_raw, &range_len); + bool type_; + napi_get_value_bool(env, args[4], &type_); + weaveffi_error err = {0}; + int32_t result = weaveffi_edge_cases_select(class, from, (const uint8_t*)func_raw, func_len, (const uint8_t*)range_raw, range_len, type_, &err); + free(from); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + napi_create_int32(env, result, &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_nested_optional(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + void* items_raw; + size_t items_len; + napi_get_buffer_info(env, args[0], &items_raw, &items_len); + weaveffi_error err = {0}; + size_t out_len; + const uint8_t* result = weaveffi_edge_cases_nested_optional((const uint8_t*)items_raw, items_len, &out_len, &err); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + napi_create_buffer_copy(env, out_len, result, NULL, &ret); + weaveffi_free_bytes((uint8_t*)result, out_len); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_optional_params(napi_env env, napi_callback_info info) { + size_t argc = 6; + napi_value args[6]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + void* text_raw; + size_t text_len; + napi_get_buffer_info(env, args[0], &text_raw, &text_len); + void* count_raw; + size_t count_len; + napi_get_buffer_info(env, args[1], &count_raw, &count_len); + void* outer_raw; + size_t outer_len; + napi_get_buffer_info(env, args[2], &outer_raw, &outer_len); + void* tags_raw; + size_t tags_len; + napi_get_buffer_info(env, args[3], &tags_raw, &tags_len); + void* blob_raw; + size_t blob_len; + napi_get_buffer_info(env, args[4], &blob_raw, &blob_len); + void* mode_raw; + size_t mode_len; + napi_get_buffer_info(env, args[5], &mode_raw, &mode_len); + weaveffi_error err = {0}; + size_t out_len; + const uint8_t* result = weaveffi_edge_cases_optional_params((const uint8_t*)text_raw, text_len, (const uint8_t*)count_raw, count_len, (const uint8_t*)outer_raw, outer_len, (const uint8_t*)tags_raw, tags_len, (const uint8_t*)blob_raw, blob_len, (const uint8_t*)mode_raw, mode_len, &out_len, &err); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + napi_create_buffer_copy(env, out_len, result, NULL, &ret); + weaveffi_free_bytes((uint8_t*)result, out_len); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_mutable_params(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + size_t text_len; + napi_get_value_string_utf8(env, args[0], NULL, 0, &text_len); + char* text = (char*)malloc(text_len + 1); + napi_get_value_string_utf8(env, args[0], text, text_len + 1, &text_len); + void* blob_raw; + size_t blob_len; + napi_get_buffer_info(env, args[1], &blob_raw, &blob_len); + weaveffi_error err = {0}; + weaveffi_edge_cases_mutable_params(text, (const uint8_t*)blob_raw, blob_len, &err); + free(text); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + napi_get_undefined(env, &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_make_widget(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + size_t label_len; + napi_get_value_string_utf8(env, args[0], NULL, 0, &label_len); + char* label = (char*)malloc(label_len + 1); + napi_get_value_string_utf8(env, args[0], label, label_len + 1, &label_len); + weaveffi_error err = {0}; + weaveffi_edge_cases_Widget* result = weaveffi_edge_cases_make_widget(label, &err); + free(label); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + napi_create_int64(env, (int64_t)(intptr_t)result, &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_find_widget(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + int64_t id; + napi_get_value_int64(env, args[0], &id); + weaveffi_error err = {0}; + weaveffi_edge_cases_Widget* result = weaveffi_edge_cases_find_widget(id, &err); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + if (result == NULL) { + napi_get_null(env, &ret); + } else { + napi_create_int64(env, (int64_t)(intptr_t)result, &ret); + } + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_inspect_widget(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + int64_t widget_raw; + napi_get_value_int64(env, args[0], &widget_raw); + napi_valuetype fallback_type; + napi_typeof(env, args[1], &fallback_type); + int64_t fallback_raw = 0; + if (fallback_type != napi_null && fallback_type != napi_undefined) { + napi_get_value_int64(env, args[1], &fallback_raw); + } + weaveffi_error err = {0}; + const char* result = weaveffi_edge_cases_inspect_widget((const weaveffi_edge_cases_Widget*)(intptr_t)widget_raw, fallback_raw ? (const weaveffi_edge_cases_Widget*)(intptr_t)fallback_raw : NULL, &err); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret); + weaveffi_free_string(result); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_typed_handle_param(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + int64_t token_raw; + napi_get_value_int64(env, args[0], &token_raw); + int64_t raw_raw; + napi_get_value_int64(env, args[1], &raw_raw); + weaveffi_error err = {0}; + weaveffi_edge_cases_Leaf* result = weaveffi_edge_cases_typed_handle_param((weaveffi_edge_cases_Leaf*)(intptr_t)token_raw, (weaveffi_handle_t)raw_raw, &err); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + napi_create_int64(env, (int64_t)(intptr_t)result, &ret); + return ret; +} + +static void weaveffi_edge_cases_string_stream_napi_iter_finalize(napi_env env, void* data, void* hint) { + (void)env; + (void)hint; + weaveffi_napi_iter_state* state = (weaveffi_napi_iter_state*)data; + if (state->iter != NULL) { + weaveffi_edge_cases_StringStreamIterator_destroy((weaveffi_edge_cases_StringStreamIterator*)state->iter); + state->iter = NULL; + } + free(state); +} + +static napi_value Napi_weaveffi_edge_cases_StringStreamIterator_next(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + void* iter_data = NULL; + napi_get_value_external(env, args[0], &iter_data); + weaveffi_napi_iter_state* state = (weaveffi_napi_iter_state*)iter_data; + napi_value ret; + if (state == NULL || state->iter == NULL) { + napi_get_undefined(env, &ret); + return ret; + } + const char* iter_item; + weaveffi_error iter_err = {0}; + if (!weaveffi_edge_cases_StringStreamIterator_next((weaveffi_edge_cases_StringStreamIterator*)state->iter, &iter_item, &iter_err)) { + weaveffi_edge_cases_StringStreamIterator_destroy((weaveffi_edge_cases_StringStreamIterator*)state->iter); + state->iter = NULL; + if (iter_err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, iter_err.code, iter_err.message, iter_err.payload_ptr, iter_err.payload_len)); + weaveffi_error_clear(&iter_err); + return NULL; + } + napi_get_undefined(env, &ret); + return ret; + } + napi_create_string_utf8(env, iter_item ? iter_item : "", NAPI_AUTO_LENGTH, &ret); + weaveffi_free_string((char*)iter_item); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_StringStreamIterator_destroy(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + void* iter_data = NULL; + napi_get_value_external(env, args[0], &iter_data); + weaveffi_napi_iter_state* state = (weaveffi_napi_iter_state*)iter_data; + if (state != NULL && state->iter != NULL) { + weaveffi_edge_cases_StringStreamIterator_destroy((weaveffi_edge_cases_StringStreamIterator*)state->iter); + state->iter = NULL; + } + napi_value ret; + napi_get_undefined(env, &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_string_stream(napi_env env, napi_callback_info info) { + size_t argc = 0; + napi_get_cb_info(env, info, &argc, NULL, NULL, NULL); + weaveffi_error err = {0}; + void* result = weaveffi_edge_cases_string_stream(&err); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + weaveffi_napi_iter_state* iter_state = (weaveffi_napi_iter_state*)calloc(1, sizeof(weaveffi_napi_iter_state)); + iter_state->iter = (void*)result; + napi_create_external(env, iter_state, weaveffi_edge_cases_string_stream_napi_iter_finalize, NULL, &ret); + return ret; +} + +static void weaveffi_edge_cases_scalar_stream_napi_iter_finalize(napi_env env, void* data, void* hint) { + (void)env; + (void)hint; + weaveffi_napi_iter_state* state = (weaveffi_napi_iter_state*)data; + if (state->iter != NULL) { + weaveffi_edge_cases_ScalarStreamIterator_destroy((weaveffi_edge_cases_ScalarStreamIterator*)state->iter); + state->iter = NULL; + } + free(state); +} + +static napi_value Napi_weaveffi_edge_cases_ScalarStreamIterator_next(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + void* iter_data = NULL; + napi_get_value_external(env, args[0], &iter_data); + weaveffi_napi_iter_state* state = (weaveffi_napi_iter_state*)iter_data; + napi_value ret; + if (state == NULL || state->iter == NULL) { + napi_get_undefined(env, &ret); + return ret; + } + int32_t iter_item; + weaveffi_error iter_err = {0}; + if (!weaveffi_edge_cases_ScalarStreamIterator_next((weaveffi_edge_cases_ScalarStreamIterator*)state->iter, &iter_item, &iter_err)) { + weaveffi_edge_cases_ScalarStreamIterator_destroy((weaveffi_edge_cases_ScalarStreamIterator*)state->iter); + state->iter = NULL; + if (iter_err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, iter_err.code, iter_err.message, iter_err.payload_ptr, iter_err.payload_len)); + weaveffi_error_clear(&iter_err); + return NULL; + } + napi_get_undefined(env, &ret); + return ret; + } + napi_create_int32(env, iter_item, &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_ScalarStreamIterator_destroy(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + void* iter_data = NULL; + napi_get_value_external(env, args[0], &iter_data); + weaveffi_napi_iter_state* state = (weaveffi_napi_iter_state*)iter_data; + if (state != NULL && state->iter != NULL) { + weaveffi_edge_cases_ScalarStreamIterator_destroy((weaveffi_edge_cases_ScalarStreamIterator*)state->iter); + state->iter = NULL; + } + napi_value ret; + napi_get_undefined(env, &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_scalar_stream(napi_env env, napi_callback_info info) { + size_t argc = 0; + napi_get_cb_info(env, info, &argc, NULL, NULL, NULL); + weaveffi_error err = {0}; + void* result = weaveffi_edge_cases_scalar_stream(&err); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + weaveffi_napi_iter_state* iter_state = (weaveffi_napi_iter_state*)calloc(1, sizeof(weaveffi_napi_iter_state)); + iter_state->iter = (void*)result; + napi_create_external(env, iter_state, weaveffi_edge_cases_scalar_stream_napi_iter_finalize, NULL, &ret); + return ret; +} + +static void weaveffi_edge_cases_mode_stream_napi_iter_finalize(napi_env env, void* data, void* hint) { + (void)env; + (void)hint; + weaveffi_napi_iter_state* state = (weaveffi_napi_iter_state*)data; + if (state->iter != NULL) { + weaveffi_edge_cases_ModeStreamIterator_destroy((weaveffi_edge_cases_ModeStreamIterator*)state->iter); + state->iter = NULL; + } + free(state); +} + +static napi_value Napi_weaveffi_edge_cases_ModeStreamIterator_next(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + void* iter_data = NULL; + napi_get_value_external(env, args[0], &iter_data); + weaveffi_napi_iter_state* state = (weaveffi_napi_iter_state*)iter_data; + napi_value ret; + if (state == NULL || state->iter == NULL) { + napi_get_undefined(env, &ret); + return ret; + } + weaveffi_edge_cases_Mode iter_item; + weaveffi_error iter_err = {0}; + if (!weaveffi_edge_cases_ModeStreamIterator_next((weaveffi_edge_cases_ModeStreamIterator*)state->iter, &iter_item, &iter_err)) { + weaveffi_edge_cases_ModeStreamIterator_destroy((weaveffi_edge_cases_ModeStreamIterator*)state->iter); + state->iter = NULL; + if (iter_err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, iter_err.code, iter_err.message, iter_err.payload_ptr, iter_err.payload_len)); + weaveffi_error_clear(&iter_err); + return NULL; + } + napi_get_undefined(env, &ret); + return ret; + } + napi_create_int32(env, (int32_t)(iter_item), &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_ModeStreamIterator_destroy(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + void* iter_data = NULL; + napi_get_value_external(env, args[0], &iter_data); + weaveffi_napi_iter_state* state = (weaveffi_napi_iter_state*)iter_data; + if (state != NULL && state->iter != NULL) { + weaveffi_edge_cases_ModeStreamIterator_destroy((weaveffi_edge_cases_ModeStreamIterator*)state->iter); + state->iter = NULL; + } + napi_value ret; + napi_get_undefined(env, &ret); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_mode_stream(napi_env env, napi_callback_info info) { + size_t argc = 0; + napi_get_cb_info(env, info, &argc, NULL, NULL, NULL); + weaveffi_error err = {0}; + void* result = weaveffi_edge_cases_mode_stream(&err); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + weaveffi_napi_iter_state* iter_state = (weaveffi_napi_iter_state*)calloc(1, sizeof(weaveffi_napi_iter_state)); + iter_state->iter = (void*)result; + napi_create_external(env, iter_state, weaveffi_edge_cases_mode_stream_napi_iter_finalize, NULL, &ret); + return ret; +} + +typedef struct { + napi_deferred deferred; + napi_threadsafe_function tsfn; + int32_t err_code; + char* err_msg; + uint8_t* err_payload; + size_t err_payload_len; +} weaveffi_edge_cases_async_void_napi_actx; + +static void weaveffi_edge_cases_async_void_napi_cb(void* context, weaveffi_error* err) { + weaveffi_edge_cases_async_void_napi_actx* ctx = (weaveffi_edge_cases_async_void_napi_actx*)context; + if (err != NULL && err->code != 0) { + ctx->err_code = err->code; + ctx->err_msg = err->message ? strdup(err->message) : strdup("unknown error"); + ctx->err_payload_len = err->payload_len; + if (err->payload_ptr != NULL && err->payload_len > 0) { ctx->err_payload = (uint8_t*)malloc(err->payload_len); memcpy(ctx->err_payload, err->payload_ptr, err->payload_len); } + } else { + } + weaveffi_error_free(err); + napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); +} + +static void weaveffi_edge_cases_async_void_napi_settle(napi_env env, napi_value js_cb, void* context, void* data) { + (void)js_cb; + (void)context; + weaveffi_edge_cases_async_void_napi_actx* ctx = (weaveffi_edge_cases_async_void_napi_actx*)data; + if (env != NULL) { + if (ctx->err_code != 0) { + napi_value err_obj = weaveffi_napi_error_value(env, ctx->err_code, ctx->err_msg, ctx->err_payload, ctx->err_payload_len); + napi_reject_deferred(env, ctx->deferred, err_obj); + } else { + napi_value val; + napi_get_undefined(env, &val); + napi_resolve_deferred(env, ctx->deferred, val); + } + } + free(ctx->err_msg); + free(ctx->err_payload); + napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release); + free(ctx); +} + +static napi_value Napi_weaveffi_edge_cases_async_void(napi_env env, napi_callback_info info) { + size_t argc = 0; + napi_get_cb_info(env, info, &argc, NULL, NULL, NULL); + weaveffi_edge_cases_async_void_napi_actx* ctx = (weaveffi_edge_cases_async_void_napi_actx*)calloc(1, sizeof(weaveffi_edge_cases_async_void_napi_actx)); + napi_value promise; + napi_create_promise(env, &ctx->deferred, &promise); + napi_value resource_name; + napi_create_string_utf8(env, "weaveffi_edge_cases_async_void", NAPI_AUTO_LENGTH, &resource_name); + napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_edge_cases_async_void_napi_settle, &ctx->tsfn); + weaveffi_edge_cases_async_void_async(weaveffi_edge_cases_async_void_napi_cb, ctx); + return promise; +} + +typedef struct { + napi_deferred deferred; + napi_threadsafe_function tsfn; + int32_t err_code; + char* err_msg; + uint8_t* err_payload; + size_t err_payload_len; + uint8_t* result; + size_t result_len; +} weaveffi_edge_cases_async_bytes_napi_actx; + +static void weaveffi_edge_cases_async_bytes_napi_cb(void* context, weaveffi_error* err, const uint8_t* result, size_t result_len) { + weaveffi_edge_cases_async_bytes_napi_actx* ctx = (weaveffi_edge_cases_async_bytes_napi_actx*)context; + if (err != NULL && err->code != 0) { + ctx->err_code = err->code; + ctx->err_msg = err->message ? strdup(err->message) : strdup("unknown error"); + ctx->err_payload_len = err->payload_len; + if (err->payload_ptr != NULL && err->payload_len > 0) { ctx->err_payload = (uint8_t*)malloc(err->payload_len); memcpy(ctx->err_payload, err->payload_ptr, err->payload_len); } + } else { + ctx->result_len = result_len; + if (result != NULL && result_len > 0) { ctx->result = (uint8_t*)malloc(result_len); memcpy(ctx->result, result, result_len); } + weaveffi_free_bytes((uint8_t*)result, result_len); + } + weaveffi_error_free(err); + napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); +} + +static void weaveffi_edge_cases_async_bytes_napi_settle(napi_env env, napi_value js_cb, void* context, void* data) { + (void)js_cb; + (void)context; + weaveffi_edge_cases_async_bytes_napi_actx* ctx = (weaveffi_edge_cases_async_bytes_napi_actx*)data; + if (env != NULL) { + if (ctx->err_code != 0) { + napi_value err_obj = weaveffi_napi_error_value(env, ctx->err_code, ctx->err_msg, ctx->err_payload, ctx->err_payload_len); + napi_reject_deferred(env, ctx->deferred, err_obj); + } else { + napi_value val; + napi_create_buffer_copy(env, ctx->result_len, ctx->result ? (const void*)ctx->result : (const void*)"", NULL, &val); + napi_resolve_deferred(env, ctx->deferred, val); + } + } + free(ctx->err_msg); + free(ctx->err_payload); + free(ctx->result); + napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release); + free(ctx); +} + +static napi_value Napi_weaveffi_edge_cases_async_bytes(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + uint32_t size; + napi_get_value_uint32(env, args[0], &size); + weaveffi_edge_cases_async_bytes_napi_actx* ctx = (weaveffi_edge_cases_async_bytes_napi_actx*)calloc(1, sizeof(weaveffi_edge_cases_async_bytes_napi_actx)); + napi_value promise; + napi_create_promise(env, &ctx->deferred, &promise); + napi_value resource_name; + napi_create_string_utf8(env, "weaveffi_edge_cases_async_bytes", NAPI_AUTO_LENGTH, &resource_name); + napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_edge_cases_async_bytes_napi_settle, &ctx->tsfn); + weaveffi_edge_cases_async_bytes_async(size, weaveffi_edge_cases_async_bytes_napi_cb, ctx); + return promise; +} + +typedef struct { + napi_deferred deferred; + napi_threadsafe_function tsfn; + int32_t err_code; + char* err_msg; + uint8_t* err_payload; + size_t err_payload_len; + uint8_t* result; + size_t result_len; +} weaveffi_edge_cases_async_struct_napi_actx; + +static void weaveffi_edge_cases_async_struct_napi_cb(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { + weaveffi_edge_cases_async_struct_napi_actx* ctx = (weaveffi_edge_cases_async_struct_napi_actx*)context; + if (err != NULL && err->code != 0) { + ctx->err_code = err->code; + ctx->err_msg = err->message ? strdup(err->message) : strdup("unknown error"); + ctx->err_payload_len = err->payload_len; + if (err->payload_ptr != NULL && err->payload_len > 0) { ctx->err_payload = (uint8_t*)malloc(err->payload_len); memcpy(ctx->err_payload, err->payload_ptr, err->payload_len); } + } else { + ctx->result_len = result_len; + if (result_ptr != NULL && result_len > 0) { ctx->result = (uint8_t*)malloc(result_len); memcpy(ctx->result, result_ptr, result_len); } + weaveffi_free_bytes((uint8_t*)result_ptr, result_len); + } + weaveffi_error_free(err); + napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); +} + +static void weaveffi_edge_cases_async_struct_napi_settle(napi_env env, napi_value js_cb, void* context, void* data) { + (void)js_cb; + (void)context; + weaveffi_edge_cases_async_struct_napi_actx* ctx = (weaveffi_edge_cases_async_struct_napi_actx*)data; + if (env != NULL) { + if (ctx->err_code != 0) { + napi_value err_obj = weaveffi_napi_error_value(env, ctx->err_code, ctx->err_msg, ctx->err_payload, ctx->err_payload_len); + napi_reject_deferred(env, ctx->deferred, err_obj); + } else { + napi_value val; + napi_create_buffer_copy(env, ctx->result_len, ctx->result ? (const void*)ctx->result : (const void*)"", NULL, &val); + napi_resolve_deferred(env, ctx->deferred, val); + } + } + free(ctx->err_msg); + free(ctx->err_payload); + free(ctx->result); + napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release); + free(ctx); +} + +static napi_value Napi_weaveffi_edge_cases_async_struct(napi_env env, napi_callback_info info) { + size_t argc = 0; + napi_get_cb_info(env, info, &argc, NULL, NULL, NULL); + weaveffi_edge_cases_async_struct_napi_actx* ctx = (weaveffi_edge_cases_async_struct_napi_actx*)calloc(1, sizeof(weaveffi_edge_cases_async_struct_napi_actx)); + napi_value promise; + napi_create_promise(env, &ctx->deferred, &promise); + napi_value resource_name; + napi_create_string_utf8(env, "weaveffi_edge_cases_async_struct", NAPI_AUTO_LENGTH, &resource_name); + napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_edge_cases_async_struct_napi_settle, &ctx->tsfn); + weaveffi_edge_cases_async_struct_async(weaveffi_edge_cases_async_struct_napi_cb, ctx); + return promise; +} + +typedef struct { + napi_deferred deferred; + napi_threadsafe_function tsfn; + int32_t err_code; + char* err_msg; + uint8_t* err_payload; + size_t err_payload_len; + uint8_t* result; + size_t result_len; +} weaveffi_edge_cases_async_list_napi_actx; + +static void weaveffi_edge_cases_async_list_napi_cb(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { + weaveffi_edge_cases_async_list_napi_actx* ctx = (weaveffi_edge_cases_async_list_napi_actx*)context; + if (err != NULL && err->code != 0) { + ctx->err_code = err->code; + ctx->err_msg = err->message ? strdup(err->message) : strdup("unknown error"); + ctx->err_payload_len = err->payload_len; + if (err->payload_ptr != NULL && err->payload_len > 0) { ctx->err_payload = (uint8_t*)malloc(err->payload_len); memcpy(ctx->err_payload, err->payload_ptr, err->payload_len); } + } else { + ctx->result_len = result_len; + if (result_ptr != NULL && result_len > 0) { ctx->result = (uint8_t*)malloc(result_len); memcpy(ctx->result, result_ptr, result_len); } + weaveffi_free_bytes((uint8_t*)result_ptr, result_len); + } + weaveffi_error_free(err); + napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); +} + +static void weaveffi_edge_cases_async_list_napi_settle(napi_env env, napi_value js_cb, void* context, void* data) { + (void)js_cb; + (void)context; + weaveffi_edge_cases_async_list_napi_actx* ctx = (weaveffi_edge_cases_async_list_napi_actx*)data; + if (env != NULL) { + if (ctx->err_code != 0) { + napi_value err_obj = weaveffi_napi_error_value(env, ctx->err_code, ctx->err_msg, ctx->err_payload, ctx->err_payload_len); + napi_reject_deferred(env, ctx->deferred, err_obj); + } else { + napi_value val; + napi_create_buffer_copy(env, ctx->result_len, ctx->result ? (const void*)ctx->result : (const void*)"", NULL, &val); + napi_resolve_deferred(env, ctx->deferred, val); + } + } + free(ctx->err_msg); + free(ctx->err_payload); + free(ctx->result); + napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release); + free(ctx); +} + +static napi_value Napi_weaveffi_edge_cases_async_list(napi_env env, napi_callback_info info) { + size_t argc = 0; + napi_get_cb_info(env, info, &argc, NULL, NULL, NULL); + weaveffi_edge_cases_async_list_napi_actx* ctx = (weaveffi_edge_cases_async_list_napi_actx*)calloc(1, sizeof(weaveffi_edge_cases_async_list_napi_actx)); + napi_value promise; + napi_create_promise(env, &ctx->deferred, &promise); + napi_value resource_name; + napi_create_string_utf8(env, "weaveffi_edge_cases_async_list", NAPI_AUTO_LENGTH, &resource_name); + napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_edge_cases_async_list_napi_settle, &ctx->tsfn); + weaveffi_edge_cases_async_list_async(NULL, weaveffi_edge_cases_async_list_napi_cb, ctx); + return promise; +} + +typedef struct { + napi_deferred deferred; + napi_threadsafe_function tsfn; + int32_t err_code; + char* err_msg; + uint8_t* err_payload; + size_t err_payload_len; + uint8_t* result; + size_t result_len; +} weaveffi_edge_cases_async_optional_napi_actx; + +static void weaveffi_edge_cases_async_optional_napi_cb(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) { + weaveffi_edge_cases_async_optional_napi_actx* ctx = (weaveffi_edge_cases_async_optional_napi_actx*)context; + if (err != NULL && err->code != 0) { + ctx->err_code = err->code; + ctx->err_msg = err->message ? strdup(err->message) : strdup("unknown error"); + ctx->err_payload_len = err->payload_len; + if (err->payload_ptr != NULL && err->payload_len > 0) { ctx->err_payload = (uint8_t*)malloc(err->payload_len); memcpy(ctx->err_payload, err->payload_ptr, err->payload_len); } + } else { + ctx->result_len = result_len; + if (result_ptr != NULL && result_len > 0) { ctx->result = (uint8_t*)malloc(result_len); memcpy(ctx->result, result_ptr, result_len); } + weaveffi_free_bytes((uint8_t*)result_ptr, result_len); + } + weaveffi_error_free(err); + napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); +} + +static void weaveffi_edge_cases_async_optional_napi_settle(napi_env env, napi_value js_cb, void* context, void* data) { + (void)js_cb; + (void)context; + weaveffi_edge_cases_async_optional_napi_actx* ctx = (weaveffi_edge_cases_async_optional_napi_actx*)data; + if (env != NULL) { + if (ctx->err_code != 0) { + napi_value err_obj = weaveffi_napi_error_value(env, ctx->err_code, ctx->err_msg, ctx->err_payload, ctx->err_payload_len); + napi_reject_deferred(env, ctx->deferred, err_obj); + } else { + napi_value val; + napi_create_buffer_copy(env, ctx->result_len, ctx->result ? (const void*)ctx->result : (const void*)"", NULL, &val); + napi_resolve_deferred(env, ctx->deferred, val); + } + } + free(ctx->err_msg); + free(ctx->err_payload); + free(ctx->result); + napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release); + free(ctx); +} + +static napi_value Napi_weaveffi_edge_cases_async_optional(napi_env env, napi_callback_info info) { + size_t argc = 0; + napi_get_cb_info(env, info, &argc, NULL, NULL, NULL); + weaveffi_edge_cases_async_optional_napi_actx* ctx = (weaveffi_edge_cases_async_optional_napi_actx*)calloc(1, sizeof(weaveffi_edge_cases_async_optional_napi_actx)); + napi_value promise; + napi_create_promise(env, &ctx->deferred, &promise); + napi_value resource_name; + napi_create_string_utf8(env, "weaveffi_edge_cases_async_optional", NAPI_AUTO_LENGTH, &resource_name); + napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_edge_cases_async_optional_napi_settle, &ctx->tsfn); + weaveffi_edge_cases_async_optional_async(weaveffi_edge_cases_async_optional_napi_cb, ctx); + return promise; +} + +typedef struct { + napi_deferred deferred; + napi_threadsafe_function tsfn; + int32_t err_code; + char* err_msg; + uint8_t* err_payload; + size_t err_payload_len; + weaveffi_edge_cases_Mode result; +} weaveffi_edge_cases_async_enum_napi_actx; + +static void weaveffi_edge_cases_async_enum_napi_cb(void* context, weaveffi_error* err, weaveffi_edge_cases_Mode result) { + weaveffi_edge_cases_async_enum_napi_actx* ctx = (weaveffi_edge_cases_async_enum_napi_actx*)context; + if (err != NULL && err->code != 0) { + ctx->err_code = err->code; + ctx->err_msg = err->message ? strdup(err->message) : strdup("unknown error"); + ctx->err_payload_len = err->payload_len; + if (err->payload_ptr != NULL && err->payload_len > 0) { ctx->err_payload = (uint8_t*)malloc(err->payload_len); memcpy(ctx->err_payload, err->payload_ptr, err->payload_len); } + } else { + ctx->result = result; + } + weaveffi_error_free(err); + napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); +} + +static void weaveffi_edge_cases_async_enum_napi_settle(napi_env env, napi_value js_cb, void* context, void* data) { + (void)js_cb; + (void)context; + weaveffi_edge_cases_async_enum_napi_actx* ctx = (weaveffi_edge_cases_async_enum_napi_actx*)data; + if (env != NULL) { + if (ctx->err_code != 0) { + napi_value err_obj = weaveffi_napi_error_value(env, ctx->err_code, ctx->err_msg, ctx->err_payload, ctx->err_payload_len); + napi_reject_deferred(env, ctx->deferred, err_obj); + } else { + napi_value val; + napi_create_int32(env, (int32_t)ctx->result, &val); + napi_resolve_deferred(env, ctx->deferred, val); + } + } + free(ctx->err_msg); + free(ctx->err_payload); + napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release); + free(ctx); +} + +static napi_value Napi_weaveffi_edge_cases_async_enum(napi_env env, napi_callback_info info) { + size_t argc = 0; + napi_get_cb_info(env, info, &argc, NULL, NULL, NULL); + weaveffi_edge_cases_async_enum_napi_actx* ctx = (weaveffi_edge_cases_async_enum_napi_actx*)calloc(1, sizeof(weaveffi_edge_cases_async_enum_napi_actx)); + napi_value promise; + napi_create_promise(env, &ctx->deferred, &promise); + napi_value resource_name; + napi_create_string_utf8(env, "weaveffi_edge_cases_async_enum", NAPI_AUTO_LENGTH, &resource_name); + napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_edge_cases_async_enum_napi_settle, &ctx->tsfn); + weaveffi_edge_cases_async_enum_async(weaveffi_edge_cases_async_enum_napi_cb, ctx); + return promise; +} + +typedef struct { + napi_deferred deferred; + napi_threadsafe_function tsfn; + int32_t err_code; + char* err_msg; + uint8_t* err_payload; + size_t err_payload_len; + char* result; + int result_null; +} weaveffi_edge_cases_async_keywords_napi_actx; + +static void weaveffi_edge_cases_async_keywords_napi_cb(void* context, weaveffi_error* err, const char* result) { + weaveffi_edge_cases_async_keywords_napi_actx* ctx = (weaveffi_edge_cases_async_keywords_napi_actx*)context; + if (err != NULL && err->code != 0) { + ctx->err_code = err->code; + ctx->err_msg = err->message ? strdup(err->message) : strdup("unknown error"); + ctx->err_payload_len = err->payload_len; + if (err->payload_ptr != NULL && err->payload_len > 0) { ctx->err_payload = (uint8_t*)malloc(err->payload_len); memcpy(ctx->err_payload, err->payload_ptr, err->payload_len); } + } else { + ctx->result_null = result == NULL; + ctx->result = result ? strdup(result) : NULL; + weaveffi_free_string(result); + } + weaveffi_error_free(err); + napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); +} + +static void weaveffi_edge_cases_async_keywords_napi_settle(napi_env env, napi_value js_cb, void* context, void* data) { + (void)js_cb; + (void)context; + weaveffi_edge_cases_async_keywords_napi_actx* ctx = (weaveffi_edge_cases_async_keywords_napi_actx*)data; + if (env != NULL) { + if (ctx->err_code != 0) { + napi_value err_obj = weaveffi_napi_error_value(env, ctx->err_code, ctx->err_msg, ctx->err_payload, ctx->err_payload_len); + napi_reject_deferred(env, ctx->deferred, err_obj); + } else { + napi_value val; + if (ctx->result_null) napi_get_null(env, &val); else napi_create_string_utf8(env, ctx->result ? ctx->result : "", NAPI_AUTO_LENGTH, &val); + napi_resolve_deferred(env, ctx->deferred, val); + } + } + free(ctx->err_msg); + free(ctx->err_payload); + free(ctx->result); + napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release); + free(ctx); +} + +static napi_value Napi_weaveffi_edge_cases_async_keywords(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value args[2]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + size_t class_len; + napi_get_value_string_utf8(env, args[0], NULL, 0, &class_len); + char* class = (char*)malloc(class_len + 1); + napi_get_value_string_utf8(env, args[0], class, class_len + 1, &class_len); + int32_t import; + napi_get_value_int32(env, args[1], &import); + weaveffi_edge_cases_async_keywords_napi_actx* ctx = (weaveffi_edge_cases_async_keywords_napi_actx*)calloc(1, sizeof(weaveffi_edge_cases_async_keywords_napi_actx)); + napi_value promise; + napi_create_promise(env, &ctx->deferred, &promise); + napi_value resource_name; + napi_create_string_utf8(env, "weaveffi_edge_cases_async_keywords", NAPI_AUTO_LENGTH, &resource_name); + napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_edge_cases_async_keywords_napi_settle, &ctx->tsfn); + weaveffi_edge_cases_async_keywords_async(class, import, weaveffi_edge_cases_async_keywords_napi_cb, ctx); + free(class); + return promise; +} + +static napi_value Napi_weaveffi_edge_cases_keywords_roundtrip(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + void* value_raw; + size_t value_len; + napi_get_buffer_info(env, args[0], &value_raw, &value_len); + weaveffi_error err = {0}; + size_t out_len; + const uint8_t* result = weaveffi_edge_cases_keywords_roundtrip((const uint8_t*)value_raw, value_len, &out_len, &err); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + napi_create_buffer_copy(env, out_len, result, NULL, &ret); + weaveffi_free_bytes((uint8_t*)result, out_len); + return ret; +} + +static napi_value Napi_weaveffi_edge_cases_legacy_value(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + int32_t value; + napi_get_value_int32(env, args[0], &value); + weaveffi_error err = {0}; + size_t out_len; + const uint8_t* result = weaveffi_edge_cases_legacy_value((weaveffi_edge_cases_Legacy)value, &out_len, &err); + if (err.code != 0) { + napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); + weaveffi_error_clear(&err); + return NULL; + } + napi_value ret; + napi_create_buffer_copy(env, out_len, result, NULL, &ret); + weaveffi_free_bytes((uint8_t*)result, out_len); + return ret; +} + +static napi_value Init(napi_env env, napi_value exports) { + { + uint32_t found = weaveffi_abi_version(); + if (found != WEAVEFFI_ABI_VERSION) { + char msg[160]; + snprintf(msg, sizeof msg, "WeaveFFI ABI mismatch: these bindings expect revision %u but the loaded library reports revision %u", (unsigned)WEAVEFFI_ABI_VERSION, (unsigned)found); + napi_throw_error(env, NULL, msg); + return NULL; + } + } + napi_property_descriptor props[] = { + { "Widget_refresh", NULL, Napi_weaveffi_edge_cases_Widget_refresh, NULL, NULL, NULL, napi_default, NULL }, + { "Widget_rename", NULL, Napi_weaveffi_edge_cases_Widget_rename, NULL, NULL, NULL, napi_default, NULL }, + { "Widget_fetch", NULL, Napi_weaveffi_edge_cases_Widget_fetch, NULL, NULL, NULL, napi_default, NULL }, + { "Widget_old_name", NULL, Napi_weaveffi_edge_cases_Widget_old_name, NULL, NULL, NULL, napi_default, NULL }, + { "Widget_destroy", NULL, Napi_weaveffi_edge_cases_Widget_destroy, NULL, NULL, NULL, napi_default, NULL }, + { "Vault_open", NULL, Napi_weaveffi_edge_cases_Vault_open, NULL, NULL, NULL, napi_default, NULL }, + { "Vault_probe", NULL, Napi_weaveffi_edge_cases_Vault_probe, NULL, NULL, NULL, napi_default, NULL }, + { "Vault_peek", NULL, Napi_weaveffi_edge_cases_Vault_peek, NULL, NULL, NULL, napi_default, NULL }, + { "Vault_destroy", NULL, Napi_weaveffi_edge_cases_Vault_destroy, NULL, NULL, NULL, napi_default, NULL }, + { "registerBatchListener", NULL, Napi_weaveffi_edge_cases_register_batch_listener, NULL, NULL, NULL, napi_default, NULL }, + { "unregisterBatchListener", NULL, Napi_weaveffi_edge_cases_unregister_batch_listener, NULL, NULL, NULL, napi_default, NULL }, + { "registerTickListener", NULL, Napi_weaveffi_edge_cases_register_tick_listener, NULL, NULL, NULL, napi_default, NULL }, + { "unregisterTickListener", NULL, Napi_weaveffi_edge_cases_unregister_tick_listener, NULL, NULL, NULL, napi_default, NULL }, + { "noop", NULL, Napi_weaveffi_edge_cases_noop, NULL, NULL, NULL, napi_default, NULL }, + { "select", NULL, Napi_weaveffi_edge_cases_select, NULL, NULL, NULL, napi_default, NULL }, + { "nestedOptional", NULL, Napi_weaveffi_edge_cases_nested_optional, NULL, NULL, NULL, napi_default, NULL }, + { "optionalParams", NULL, Napi_weaveffi_edge_cases_optional_params, NULL, NULL, NULL, napi_default, NULL }, + { "mutableParams", NULL, Napi_weaveffi_edge_cases_mutable_params, NULL, NULL, NULL, napi_default, NULL }, + { "makeWidget", NULL, Napi_weaveffi_edge_cases_make_widget, NULL, NULL, NULL, napi_default, NULL }, + { "findWidget", NULL, Napi_weaveffi_edge_cases_find_widget, NULL, NULL, NULL, napi_default, NULL }, + { "inspectWidget", NULL, Napi_weaveffi_edge_cases_inspect_widget, NULL, NULL, NULL, napi_default, NULL }, + { "typedHandleParam", NULL, Napi_weaveffi_edge_cases_typed_handle_param, NULL, NULL, NULL, napi_default, NULL }, + { "stringStream_iterNext", NULL, Napi_weaveffi_edge_cases_StringStreamIterator_next, NULL, NULL, NULL, napi_default, NULL }, + { "stringStream_iterDestroy", NULL, Napi_weaveffi_edge_cases_StringStreamIterator_destroy, NULL, NULL, NULL, napi_default, NULL }, + { "stringStream", NULL, Napi_weaveffi_edge_cases_string_stream, NULL, NULL, NULL, napi_default, NULL }, + { "scalarStream_iterNext", NULL, Napi_weaveffi_edge_cases_ScalarStreamIterator_next, NULL, NULL, NULL, napi_default, NULL }, + { "scalarStream_iterDestroy", NULL, Napi_weaveffi_edge_cases_ScalarStreamIterator_destroy, NULL, NULL, NULL, napi_default, NULL }, + { "scalarStream", NULL, Napi_weaveffi_edge_cases_scalar_stream, NULL, NULL, NULL, napi_default, NULL }, + { "modeStream_iterNext", NULL, Napi_weaveffi_edge_cases_ModeStreamIterator_next, NULL, NULL, NULL, napi_default, NULL }, + { "modeStream_iterDestroy", NULL, Napi_weaveffi_edge_cases_ModeStreamIterator_destroy, NULL, NULL, NULL, napi_default, NULL }, + { "modeStream", NULL, Napi_weaveffi_edge_cases_mode_stream, NULL, NULL, NULL, napi_default, NULL }, + { "asyncVoid", NULL, Napi_weaveffi_edge_cases_async_void, NULL, NULL, NULL, napi_default, NULL }, + { "asyncBytes", NULL, Napi_weaveffi_edge_cases_async_bytes, NULL, NULL, NULL, napi_default, NULL }, + { "asyncStruct", NULL, Napi_weaveffi_edge_cases_async_struct, NULL, NULL, NULL, napi_default, NULL }, + { "asyncList", NULL, Napi_weaveffi_edge_cases_async_list, NULL, NULL, NULL, napi_default, NULL }, + { "asyncOptional", NULL, Napi_weaveffi_edge_cases_async_optional, NULL, NULL, NULL, napi_default, NULL }, + { "asyncEnum", NULL, Napi_weaveffi_edge_cases_async_enum, NULL, NULL, NULL, napi_default, NULL }, + { "asyncKeywords", NULL, Napi_weaveffi_edge_cases_async_keywords, NULL, NULL, NULL, napi_default, NULL }, + { "keywordsRoundtrip", NULL, Napi_weaveffi_edge_cases_keywords_roundtrip, NULL, NULL, NULL, napi_default, NULL }, + { "legacyValue", NULL, Napi_weaveffi_edge_cases_legacy_value, NULL, NULL, NULL, napi_default, NULL }, + }; + napi_define_properties(env, exports, 40, props); + return exports; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) + +// END weaveffi_addon.c diff --git a/crates/weaveffi-cli/tests/snapshots/node_events__binding_gyp.snap b/crates/weaveffi-cli/tests/snapshots/node_events__binding_gyp.snap deleted file mode 100644 index 3997b5bb..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_events__binding_gyp.snap +++ /dev/null @@ -1,19 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -{ - "targets": [ - { - "target_name": "weaveffi", - "sources": ["weaveffi_addon.c"], - "include_dirs": ["../c"], - "libraries": ["-lweaveffi"] - } - ] -} - -# END binding.gyp diff --git a/crates/weaveffi-cli/tests/snapshots/node_events__index_js.snap b/crates/weaveffi-cli/tests/snapshots/node_events__index_js.snap deleted file mode 100644 index c38c381d..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_events__index_js.snap +++ /dev/null @@ -1,104 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// The WEAVEFFI_ADDON environment variable overrides the addon location -// (an absolute path to the built .node file); otherwise prefer the -// default node-gyp output path and fall back to a prebuilt index.node -// placed next to this file. -let addon; -if (process.env.WEAVEFFI_ADDON) { - addon = require(process.env.WEAVEFFI_ADDON); -} else { - try { - addon = require('./build/Release/weaveffi.node'); - } catch (e) { - addon = require('./index.node'); - } -} - -// Re-export every native binding, then layer the idiomatic wrappers -// (error classes, interface classes, buffer pack/unpack, function -// wrappers) on top. -const wv = {}; -for (const _name of Object.getOwnPropertyNames(addon)) { - wv[_name] = addon[_name]; -} - -class WeaveFFIError extends Error { - constructor(code, message) { - super('(' + code + ') ' + (message || '')); - this.name = 'WeaveFFIError'; - this.code = code; - this.errorMessage = message || ''; - } -} -wv.WeaveFFIError = WeaveFFIError; -function __generic(code, message) { - return new WeaveFFIError(code, message); -} -function __rebrand(e, map) { - return e && typeof e.code === 'number' ? map(e.code, e.message, e.payload) : e; -} -function __invoke(fn, args, map) { - try { - return fn.apply(null, args); - } catch (e) { - throw __rebrand(e, map); - } -} -function __invokeAsync(fn, args, map) { - return fn.apply(null, args).catch((e) => { - throw __rebrand(e, map); - }); -} - -// Lazy iterator over a native producer: one native `next` per step. -// The native handle is released on exhaustion, by `return()` on early -// exit, or by the external's finalizer if the iterator is abandoned. -class WeaveFFIIterator { - constructor(ext, nextFn, destroyFn, map, wrapElem) { - this._ext = ext; - this._nextFn = nextFn; - this._destroyFn = destroyFn; - this._map = map; - this._wrapElem = wrapElem; - this._done = false; - } - next() { - if (this._done) { - return { done: true, value: undefined }; - } - const _v = __invoke(this._nextFn, [this._ext], this._map); - if (_v === undefined) { - this._done = true; - return { done: true, value: undefined }; - } - return { done: false, value: this._wrapElem ? this._wrapElem(_v) : _v }; - } - return(value) { - if (!this._done) { - this._done = true; - this._destroyFn(this._ext); - } - return { done: true, value }; - } - [Symbol.iterator]() { - return this; - } -} - -wv.sendMessage = function (text) { - return __invoke(addon.sendMessage, [text], __generic); -}; -wv.getMessages = function () { - const _it = __invoke(addon.getMessages, [], __generic); - return new WeaveFFIIterator(_it, addon.getMessages_iterNext, addon.getMessages_iterDestroy, __generic, null); -}; - -module.exports = wv; - -// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_events__package_json.snap b/crates/weaveffi-cli/tests/snapshots/node_events__package_json.snap deleted file mode 100644 index 6fd366d2..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_events__package_json.snap +++ /dev/null @@ -1,17 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -{ - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", - "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", - "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", - "main": "index.js", - "types": "types.d.ts", - "gypfile": true, - "scripts": { - "install": "node-gyp rebuild" - } -} diff --git a/crates/weaveffi-cli/tests/snapshots/node_events__types_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_events__types_d_ts.snap deleted file mode 100644 index f0e6c9e8..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_events__types_d_ts.snap +++ /dev/null @@ -1,35 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// Generated types for WeaveFFI functions -/** - * Base class of every error thrown by these bindings. Non-throwing - * functions reject or throw it directly for panics and marshalling - * failures; throwing functions surface a module domain subclass. - */ -export class WeaveFFIError extends Error { - /** The numeric ABI error code. */ - code: number; - /** The raw producer message, without the code prefix. */ - errorMessage: string; - constructor(code: number, message: string); -} -// module events -export function registerMessageListener(callback: (message: string) => void): number -export function unregisterMessageListener(id: number): void -/** - * Send a message, triggering the OnMessage callback - * Maps to C function: weaveffi_events_send_message - */ -export function sendMessage(text: string): void -/** - * Return an iterator over all sent messages - * Maps to C function: weaveffi_events_get_messages - */ -export function getMessages(): IterableIterator - -// END types.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/node_events__weaveffi_addon_c.snap b/crates/weaveffi-cli/tests/snapshots/node_events__weaveffi_addon_c.snap deleted file mode 100644 index e1688e0a..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_events__weaveffi_addon_c.snap +++ /dev/null @@ -1,218 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include -#include "weaveffi.h" -#include -#include - -static napi_value weaveffi_napi_error_value(napi_env env, int32_t code, const char* message, const uint8_t* payload_ptr, size_t payload_len) { - napi_value msg; - napi_create_string_utf8(env, message ? message : "", NAPI_AUTO_LENGTH, &msg); - napi_value err; - napi_create_error(env, NULL, msg, &err); - napi_value code_val; - napi_create_int32(env, code, &code_val); - napi_set_named_property(env, err, "code", code_val); - if (payload_ptr != NULL) { - napi_value payload_val; - napi_create_buffer_copy(env, payload_len, payload_ptr, NULL, &payload_val); - napi_set_named_property(env, err, "payload", payload_val); - } - return err; -} - -typedef struct { - void* iter; -} weaveffi_napi_iter_state; - -typedef struct weaveffi_napi_listener_ctx { - napi_threadsafe_function tsfn; - uint64_t id; - struct weaveffi_napi_listener_ctx* next; -} weaveffi_napi_listener_ctx; - -static weaveffi_napi_listener_ctx* weaveffi_napi_listeners = NULL; - -typedef struct { - char* message; -} weaveffi_events_OnMessage_fn_payload; - -static void weaveffi_events_OnMessage_fn_napi_tramp(const char* message, void* context) { - weaveffi_napi_listener_ctx* ctx = (weaveffi_napi_listener_ctx*)context; - weaveffi_events_OnMessage_fn_payload* p = (weaveffi_events_OnMessage_fn_payload*)calloc(1, sizeof(weaveffi_events_OnMessage_fn_payload)); - p->message = message ? strdup(message) : NULL; - napi_call_threadsafe_function(ctx->tsfn, p, napi_tsfn_nonblocking); -} - -static void weaveffi_events_OnMessage_fn_napi_calljs(napi_env env, napi_value js_cb, void* context, void* data) { - (void)context; - weaveffi_events_OnMessage_fn_payload* p = (weaveffi_events_OnMessage_fn_payload*)data; - if (env != NULL) { - napi_value undefined; - napi_get_undefined(env, &undefined); - napi_value argv[1]; - napi_create_string_utf8(env, p->message ? p->message : "", NAPI_AUTO_LENGTH, &argv[0]); - napi_call_function(env, undefined, js_cb, 1, argv, NULL); - } - free(p->message); - free(p); -} - -static napi_value Napi_weaveffi_events_register_message_listener(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - weaveffi_napi_listener_ctx* ctx = (weaveffi_napi_listener_ctx*)calloc(1, sizeof(weaveffi_napi_listener_ctx)); - napi_value resource_name; - napi_create_string_utf8(env, "weaveffi_events_register_message_listener", NAPI_AUTO_LENGTH, &resource_name); - napi_create_threadsafe_function(env, args[0], NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_events_OnMessage_fn_napi_calljs, &ctx->tsfn); - napi_unref_threadsafe_function(env, ctx->tsfn); - uint64_t id = weaveffi_events_register_message_listener(weaveffi_events_OnMessage_fn_napi_tramp, ctx); - ctx->id = id; - ctx->next = weaveffi_napi_listeners; - weaveffi_napi_listeners = ctx; - napi_value ret; - napi_create_double(env, (double)id, &ret); - return ret; -} - -static napi_value Napi_weaveffi_events_unregister_message_listener(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - double id_d = 0; - napi_get_value_double(env, args[0], &id_d); - uint64_t id = (uint64_t)id_d; - weaveffi_events_unregister_message_listener(id); - weaveffi_napi_listener_ctx** link = &weaveffi_napi_listeners; - while (*link != NULL) { - if ((*link)->id == id) { - weaveffi_napi_listener_ctx* found = *link; - *link = found->next; - napi_release_threadsafe_function(found->tsfn, napi_tsfn_release); - free(found); - break; - } - link = &(*link)->next; - } - napi_value ret; - napi_get_undefined(env, &ret); - return ret; -} - -static napi_value Napi_weaveffi_events_send_message(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - size_t text_len; - napi_get_value_string_utf8(env, args[0], NULL, 0, &text_len); - char* text = (char*)malloc(text_len + 1); - napi_get_value_string_utf8(env, args[0], text, text_len + 1, &text_len); - weaveffi_error err = {0}; - weaveffi_events_send_message(text, &err); - free(text); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_get_undefined(env, &ret); - return ret; -} - -static void weaveffi_events_get_messages_napi_iter_finalize(napi_env env, void* data, void* hint) { - (void)env; - (void)hint; - weaveffi_napi_iter_state* state = (weaveffi_napi_iter_state*)data; - if (state->iter != NULL) { - weaveffi_events_GetMessagesIterator_destroy((weaveffi_events_GetMessagesIterator*)state->iter); - state->iter = NULL; - } - free(state); -} - -static napi_value Napi_weaveffi_events_GetMessagesIterator_next(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - void* iter_data = NULL; - napi_get_value_external(env, args[0], &iter_data); - weaveffi_napi_iter_state* state = (weaveffi_napi_iter_state*)iter_data; - napi_value ret; - if (state == NULL || state->iter == NULL) { - napi_get_undefined(env, &ret); - return ret; - } - const char* iter_item; - weaveffi_error iter_err = {0}; - if (!weaveffi_events_GetMessagesIterator_next((weaveffi_events_GetMessagesIterator*)state->iter, &iter_item, &iter_err)) { - weaveffi_events_GetMessagesIterator_destroy((weaveffi_events_GetMessagesIterator*)state->iter); - state->iter = NULL; - if (iter_err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, iter_err.code, iter_err.message, iter_err.payload_ptr, iter_err.payload_len)); - weaveffi_error_clear(&iter_err); - return NULL; - } - napi_get_undefined(env, &ret); - return ret; - } - napi_create_string_utf8(env, iter_item ? iter_item : "", NAPI_AUTO_LENGTH, &ret); - weaveffi_free_string((char*)iter_item); - return ret; -} - -static napi_value Napi_weaveffi_events_GetMessagesIterator_destroy(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - void* iter_data = NULL; - napi_get_value_external(env, args[0], &iter_data); - weaveffi_napi_iter_state* state = (weaveffi_napi_iter_state*)iter_data; - if (state != NULL && state->iter != NULL) { - weaveffi_events_GetMessagesIterator_destroy((weaveffi_events_GetMessagesIterator*)state->iter); - state->iter = NULL; - } - napi_value ret; - napi_get_undefined(env, &ret); - return ret; -} - -static napi_value Napi_weaveffi_events_get_messages(napi_env env, napi_callback_info info) { - size_t argc = 0; - napi_get_cb_info(env, info, &argc, NULL, NULL, NULL); - weaveffi_error err = {0}; - void* result = weaveffi_events_get_messages(&err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - weaveffi_napi_iter_state* iter_state = (weaveffi_napi_iter_state*)calloc(1, sizeof(weaveffi_napi_iter_state)); - iter_state->iter = (void*)result; - napi_create_external(env, iter_state, weaveffi_events_get_messages_napi_iter_finalize, NULL, &ret); - return ret; -} - -static napi_value Init(napi_env env, napi_value exports) { - napi_property_descriptor props[] = { - { "registerMessageListener", NULL, Napi_weaveffi_events_register_message_listener, NULL, NULL, NULL, napi_default, NULL }, - { "unregisterMessageListener", NULL, Napi_weaveffi_events_unregister_message_listener, NULL, NULL, NULL, napi_default, NULL }, - { "sendMessage", NULL, Napi_weaveffi_events_send_message, NULL, NULL, NULL, napi_default, NULL }, - { "getMessages_iterNext", NULL, Napi_weaveffi_events_GetMessagesIterator_next, NULL, NULL, NULL, napi_default, NULL }, - { "getMessages_iterDestroy", NULL, Napi_weaveffi_events_GetMessagesIterator_destroy, NULL, NULL, NULL, napi_default, NULL }, - { "getMessages", NULL, Napi_weaveffi_events_get_messages, NULL, NULL, NULL, napi_default, NULL }, - }; - napi_define_properties(env, exports, 6, props); - return exports; -} - -NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) - -// END weaveffi_addon.c diff --git a/crates/weaveffi-cli/tests/snapshots/node_inventory__binding_gyp.snap b/crates/weaveffi-cli/tests/snapshots/node_inventory__binding_gyp.snap deleted file mode 100644 index 3997b5bb..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_inventory__binding_gyp.snap +++ /dev/null @@ -1,19 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -{ - "targets": [ - { - "target_name": "weaveffi", - "sources": ["weaveffi_addon.c"], - "include_dirs": ["../c"], - "libraries": ["-lweaveffi"] - } - ] -} - -# END binding.gyp diff --git a/crates/weaveffi-cli/tests/snapshots/node_inventory__index_js.snap b/crates/weaveffi-cli/tests/snapshots/node_inventory__index_js.snap deleted file mode 100644 index 93099ab8..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_inventory__index_js.snap +++ /dev/null @@ -1,344 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// The WEAVEFFI_ADDON environment variable overrides the addon location -// (an absolute path to the built .node file); otherwise prefer the -// default node-gyp output path and fall back to a prebuilt index.node -// placed next to this file. -let addon; -if (process.env.WEAVEFFI_ADDON) { - addon = require(process.env.WEAVEFFI_ADDON); -} else { - try { - addon = require('./build/Release/weaveffi.node'); - } catch (e) { - addon = require('./index.node'); - } -} - -// Re-export every native binding, then layer the idiomatic wrappers -// (error classes, interface classes, buffer pack/unpack, function -// wrappers) on top. -const wv = {}; -for (const _name of Object.getOwnPropertyNames(addon)) { - wv[_name] = addon[_name]; -} - -class WeaveFFIError extends Error { - constructor(code, message) { - super('(' + code + ') ' + (message || '')); - this.name = 'WeaveFFIError'; - this.code = code; - this.errorMessage = message || ''; - } -} -wv.WeaveFFIError = WeaveFFIError; -function __generic(code, message) { - return new WeaveFFIError(code, message); -} -function __rebrand(e, map) { - return e && typeof e.code === 'number' ? map(e.code, e.message, e.payload) : e; -} -function __invoke(fn, args, map) { - try { - return fn.apply(null, args); - } catch (e) { - throw __rebrand(e, map); - } -} -function __invokeAsync(fn, args, map) { - return fn.apply(null, args).catch((e) => { - throw __rebrand(e, map); - }); -} - -// --- Private value-buffer runtime (WeaveFFI wire format) -------------------- -// Little-endian, packed, no alignment. Decoders reject truncated buffers, -// invalid bool/flag bytes, hostile length prefixes, and trailing bytes. -const __utf8 = new TextDecoder('utf-8', { fatal: true }); -function __bufferError(what) { - return new WeaveFFIError(-2, 'malformed value buffer: ' + what); -} -class __Writer { - constructor() { - this._buf = Buffer.alloc(256); - this._len = 0; - } - _reserve(n) { - if (this._len + n <= this._buf.length) return; - let cap = this._buf.length; - while (cap < this._len + n) cap *= 2; - const grown = Buffer.alloc(cap); - this._buf.copy(grown, 0, 0, this._len); - this._buf = grown; - } - bool(v) { this._reserve(1); this._buf[this._len++] = v ? 1 : 0; } - i8(v) { this._reserve(1); this._buf.writeInt8(v, this._len); this._len += 1; } - u8(v) { this._reserve(1); this._buf.writeUInt8(v, this._len); this._len += 1; } - i16(v) { this._reserve(2); this._buf.writeInt16LE(v, this._len); this._len += 2; } - u16(v) { this._reserve(2); this._buf.writeUInt16LE(v, this._len); this._len += 2; } - i32(v) { this._reserve(4); this._buf.writeInt32LE(v, this._len); this._len += 4; } - u32(v) { this._reserve(4); this._buf.writeUInt32LE(v, this._len); this._len += 4; } - i64(v) { this._reserve(8); this._buf.writeBigInt64LE(BigInt(v), this._len); this._len += 8; } - u64(v) { this._reserve(8); this._buf.writeBigUInt64LE(BigInt(v), this._len); this._len += 8; } - f32(v) { this._reserve(4); this._buf.writeFloatLE(v, this._len); this._len += 4; } - f64(v) { this._reserve(8); this._buf.writeDoubleLE(v, this._len); this._len += 8; } - str(v) { - const b = Buffer.from(String(v), 'utf8'); - this.u32(b.length); - this._reserve(b.length); - b.copy(this._buf, this._len); - this._len += b.length; - } - bytes(v) { - this.u32(v.length); - this._reserve(v.length); - this._buf.set(v, this._len); - this._len += v.length; - } - finish() { return this._buf.subarray(0, this._len); } -} -class __Reader { - constructor(buf) { - this._buf = Buffer.isBuffer(buf) ? buf : Buffer.from(buf); - this._pos = 0; - } - _take(n, what) { - if (this._pos + n > this._buf.length) throw __bufferError(what); - const at = this._pos; - this._pos += n; - return at; - } - bool() { - const b = this._buf[this._take(1, 'bool')]; - if (b > 1) throw __bufferError('bool byte out of range'); - return b === 1; - } - i8() { return this._buf.readInt8(this._take(1, 'i8')); } - u8() { return this._buf.readUInt8(this._take(1, 'u8')); } - i16() { return this._buf.readInt16LE(this._take(2, 'i16')); } - u16() { return this._buf.readUInt16LE(this._take(2, 'u16')); } - i32() { return this._buf.readInt32LE(this._take(4, 'i32')); } - u32() { return this._buf.readUInt32LE(this._take(4, 'u32')); } - i64() { return this._buf.readBigInt64LE(this._take(8, 'i64')); } - u64() { return this._buf.readBigUInt64LE(this._take(8, 'u64')); } - f32() { return this._buf.readFloatLE(this._take(4, 'f32')); } - f64() { return this._buf.readDoubleLE(this._take(8, 'f64')); } - len() { - const n = this.u32(); - if (n > this._buf.length - this._pos) throw __bufferError('length prefix exceeds remaining bytes'); - return n; - } - str() { - const n = this.len(); - const at = this._take(n, 'string bytes'); - try { - return __utf8.decode(this._buf.subarray(at, at + n)); - } catch (e) { - throw __bufferError('string is not valid UTF-8'); - } - } - bytes() { - const n = this.len(); - const at = this._take(n, 'byte buffer'); - return Buffer.from(this._buf.subarray(at, at + n)); - } - end() { - if (this._pos !== this._buf.length) throw __bufferError('trailing bytes after value'); - } -} -function __encode(f, v) { const w = new __Writer(); f(w, v); return w.finish(); } -function __decode(f, b) { const r = new __Reader(b); const v = f(r); r.end(); return v; } -function __wOpt(w, v, f) { if (v === null || v === undefined) { w.bool(false); } else { w.bool(true); f(w, v); } } -function __rOpt(r, f) { return r.bool() ? f(r) : null; } -function __wList(w, v, f) { w.u32(v.length); for (const e of v) f(w, e); } -function __rList(r, f) { - const n = r.len(); - const out = []; - for (let i = 0; i < n; i++) out.push(f(r)); - return out; -} -function __wMap(w, v, kf, vf) { - const keys = Object.keys(v); - w.u32(keys.length); - for (const k of keys) { kf(w, k); vf(w, v[k]); } -} -function __rMap(r, kf, vf) { - const n = r.len(); - const out = {}; - for (let i = 0; i < n; i++) { - const k = kf(r); - out[k] = vf(r); - } - return out; -} - -function __packProduct(w, v) { - w.i64(v.id); - w.str(v.name); - __wOpt(w, v.description, (w, v) => w.str(v)); - w.f64(v.price); - w.i32(v.category); - __wList(w, v.tags, (w, v) => w.str(v)); -} -function __unpackProduct(r) { - return { - id: Number(r.i64()), - name: r.str(), - description: __rOpt(r, (r) => r.str()), - price: r.f64(), - category: r.i32(), - tags: __rList(r, (r) => r.str()), - }; -} - -function __packOrderItem(w, v) { - w.i64(v.product_id); - w.i32(v.quantity); - w.f64(v.unit_price); -} -function __unpackOrderItem(r) { - return { - product_id: Number(r.i64()), - quantity: r.i32(), - unit_price: r.f64(), - }; -} -function __packOrder(w, v) { - w.i64(v.id); - __wList(w, v.items, __packOrderItem); - w.f64(v.total); - w.str(v.status); -} -function __unpackOrder(r) { - return { - id: Number(r.i64()), - items: __rList(r, __unpackOrderItem), - total: r.f64(), - status: r.str(), - }; -} - -class ProductsError extends WeaveFFIError { - constructor(code, message) { - super(code, message); - this.name = 'ProductsError'; - } -} -wv.ProductsError = ProductsError; -class InvalidPriceError extends ProductsError { - constructor(message) { - super(1, message || 'price must be positive'); - this.name = 'InvalidPriceError'; - } -} -InvalidPriceError.CODE = 1; -wv.InvalidPriceError = InvalidPriceError; -class ProductNotFoundError extends ProductsError { - constructor(message) { - super(2, message || 'product not found'); - this.name = 'ProductNotFoundError'; - } -} -ProductNotFoundError.CODE = 2; -wv.ProductNotFoundError = ProductNotFoundError; -const __productsErrorCodes = Object.freeze({ 1: InvalidPriceError, 2: ProductNotFoundError }); -function __productsErrorFrom(code, message, payload) { - const _cls = __productsErrorCodes[code]; - const _err = _cls === undefined ? new WeaveFFIError(code, message) : new _cls(message); - return _err; -} - -class Catalog { - constructor() { - this._handle = __invoke(addon.Catalog_new, [], __generic); - Catalog._cleanup.register(this, this._handle, this); - } - addProduct(name, price, category) { - const _r = __invoke(addon.Catalog_add_product, [this._handle, name, price, category], __productsErrorFrom); - return __decode(__unpackProduct, _r); - } - getProduct(id) { - const _r = __invoke(addon.Catalog_get_product, [this._handle, id], __productsErrorFrom); - return __decode(__unpackProduct, _r); - } - search(category) { - const _r = __invoke(addon.Catalog_search, [this._handle, category], __generic); - return __decode((r) => __rList(r, __unpackProduct), _r); - } - updatePrice(id, price) { - return __invoke(addon.Catalog_update_price, [this._handle, id, price], __productsErrorFrom); - } - remove(id) { - return __invoke(addon.Catalog_remove, [this._handle, id], __generic); - } - destroy() { - if (this._handle) { - Catalog._cleanup.unregister(this); - addon.Catalog_destroy(this._handle); - this._handle = 0; - } - } -} -Catalog._fromHandle = function (handle) { - const _o = Object.create(Catalog.prototype); - _o._handle = handle; - Catalog._cleanup.register(_o, handle, _o); - return _o; -}; -Catalog._cleanup = new FinalizationRegistry((handle) => { - if (handle) { addon.Catalog_destroy(handle); } -}); -wv.Catalog = Catalog; - -class OrdersError extends WeaveFFIError { - constructor(code, message) { - super(code, message); - this.name = 'OrdersError'; - } -} -wv.OrdersError = OrdersError; -class OrderNotFoundError extends OrdersError { - constructor(message) { - super(1, message || 'order not found'); - this.name = 'OrderNotFoundError'; - } -} -OrderNotFoundError.CODE = 1; -wv.OrderNotFoundError = OrderNotFoundError; -class EmptyOrderError extends OrdersError { - constructor(message) { - super(2, message || 'order must contain at least one item'); - this.name = 'EmptyOrderError'; - } -} -EmptyOrderError.CODE = 2; -wv.EmptyOrderError = EmptyOrderError; -const __ordersErrorCodes = Object.freeze({ 1: OrderNotFoundError, 2: EmptyOrderError }); -function __ordersErrorFrom(code, message, payload) { - const _cls = __ordersErrorCodes[code]; - const _err = _cls === undefined ? new WeaveFFIError(code, message) : new _cls(message); - return _err; -} - -wv.createOrder = function (items) { - return __invoke(addon.createOrder, [__encode((w, v) => __wList(w, v, __packOrderItem), items)], __ordersErrorFrom); -}; -wv.getOrder = function (id) { - const _r = __invoke(addon.getOrder, [id], __ordersErrorFrom); - return __decode(__unpackOrder, _r); -}; -wv.cancelOrder = function (id) { - return __invoke(addon.cancelOrder, [id], __generic); -}; -wv.addProductToOrder = function (orderId, product) { - return __invoke(addon.addProductToOrder, [orderId, __encode(__packProduct, product)], __generic); -}; - -module.exports = wv; - -// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_inventory__package_json.snap b/crates/weaveffi-cli/tests/snapshots/node_inventory__package_json.snap deleted file mode 100644 index 6fd366d2..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_inventory__package_json.snap +++ /dev/null @@ -1,17 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -{ - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", - "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", - "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", - "main": "index.js", - "types": "types.d.ts", - "gypfile": true, - "scripts": { - "install": "node-gyp rebuild" - } -} diff --git a/crates/weaveffi-cli/tests/snapshots/node_inventory__types_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_inventory__types_d_ts.snap deleted file mode 100644 index 82fba6ea..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_inventory__types_d_ts.snap +++ /dev/null @@ -1,123 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// Generated types for WeaveFFI functions -/** - * Base class of every error thrown by these bindings. Non-throwing - * functions reject or throw it directly for panics and marshalling - * failures; throwing functions surface a module domain subclass. - */ -export class WeaveFFIError extends Error { - /** The numeric ABI error code. */ - code: number; - /** The raw producer message, without the code prefix. */ - errorMessage: string; - constructor(code: number, message: string); -} -/** Typed errors reported by the `products` module's throwing functions. */ -export class ProductsError extends WeaveFFIError { - constructor(code: number, message: string); -} -export class InvalidPriceError extends ProductsError { - static readonly CODE: 1; - constructor(message?: string); -} -export class ProductNotFoundError extends ProductsError { - static readonly CODE: 2; - constructor(message?: string); -} -export interface Product { - id: number; - name: string; - description: string | null; - price: number; - category: Category; - tags: string[]; -} -export enum Category { - Electronics = 0, - Clothing = 1, - Food = 2, - Books = 3, -} -// module products -/** A product catalog owning its products */ -export class Catalog { - /** - * Maps to C function: weaveffi_products_Catalog_new - */ - constructor(); - /** - * Maps to C function: weaveffi_products_Catalog_add_product - * @throws {ProductsError} - */ - addProduct(name: string, price: number, category: Category): Product; - /** - * Maps to C function: weaveffi_products_Catalog_get_product - * @throws {ProductsError} - */ - getProduct(id: number): Product; - /** - * Maps to C function: weaveffi_products_Catalog_search - */ - search(category: Category): Product[]; - /** - * Maps to C function: weaveffi_products_Catalog_update_price - * @throws {ProductsError} - */ - updatePrice(id: number, price: number): boolean; - /** - * Maps to C function: weaveffi_products_Catalog_remove - */ - remove(id: number): boolean; - /** Free the underlying native object. */ - destroy(): void; -} -/** Typed errors reported by the `orders` module's throwing functions. */ -export class OrdersError extends WeaveFFIError { - constructor(code: number, message: string); -} -export class OrderNotFoundError extends OrdersError { - static readonly CODE: 1; - constructor(message?: string); -} -export class EmptyOrderError extends OrdersError { - static readonly CODE: 2; - constructor(message?: string); -} -export interface OrderItem { - product_id: number; - quantity: number; - unit_price: number; -} -export interface Order { - id: number; - items: OrderItem[]; - total: number; - status: string; -} -// module orders -/** - * Maps to C function: weaveffi_orders_create_order - * @throws {OrdersError} - */ -export function createOrder(items: OrderItem[]): number -/** - * Maps to C function: weaveffi_orders_get_order - * @throws {OrdersError} - */ -export function getOrder(id: number): Order -/** - * Maps to C function: weaveffi_orders_cancel_order - */ -export function cancelOrder(id: number): boolean -/** - * Maps to C function: weaveffi_orders_add_product_to_order - */ -export function addProductToOrder(orderId: number, product: Product): boolean - -// END types.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/node_inventory__weaveffi_addon_c.snap b/crates/weaveffi-cli/tests/snapshots/node_inventory__weaveffi_addon_c.snap deleted file mode 100644 index d121c85a..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_inventory__weaveffi_addon_c.snap +++ /dev/null @@ -1,270 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include -#include "weaveffi.h" -#include -#include - -static napi_value weaveffi_napi_error_value(napi_env env, int32_t code, const char* message, const uint8_t* payload_ptr, size_t payload_len) { - napi_value msg; - napi_create_string_utf8(env, message ? message : "", NAPI_AUTO_LENGTH, &msg); - napi_value err; - napi_create_error(env, NULL, msg, &err); - napi_value code_val; - napi_create_int32(env, code, &code_val); - napi_set_named_property(env, err, "code", code_val); - if (payload_ptr != NULL) { - napi_value payload_val; - napi_create_buffer_copy(env, payload_len, payload_ptr, NULL, &payload_val); - napi_set_named_property(env, err, "payload", payload_val); - } - return err; -} - -static napi_value Napi_weaveffi_products_Catalog_new(napi_env env, napi_callback_info info) { - size_t argc = 0; - napi_get_cb_info(env, info, &argc, NULL, NULL, NULL); - weaveffi_error err = {0}; - weaveffi_products_Catalog* result = weaveffi_products_Catalog_new(&err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_int64(env, (int64_t)(intptr_t)result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_products_Catalog_add_product(napi_env env, napi_callback_info info) { - size_t argc = 4; - napi_value args[4]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - size_t name_len; - napi_get_value_string_utf8(env, args[1], NULL, 0, &name_len); - char* name = (char*)malloc(name_len + 1); - napi_get_value_string_utf8(env, args[1], name, name_len + 1, &name_len); - double price; - napi_get_value_double(env, args[2], &price); - int32_t category; - napi_get_value_int32(env, args[3], &category); - weaveffi_error err = {0}; - size_t out_len; - const uint8_t* result = weaveffi_products_Catalog_add_product((const weaveffi_products_Catalog*)(intptr_t)self_raw, name, price, (weaveffi_products_Category)category, &out_len, &err); - free(name); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_buffer_copy(env, out_len, result, NULL, &ret); - weaveffi_free_bytes((uint8_t*)result, out_len); - return ret; -} - -static napi_value Napi_weaveffi_products_Catalog_get_product(napi_env env, napi_callback_info info) { - size_t argc = 2; - napi_value args[2]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - int64_t id; - napi_get_value_int64(env, args[1], &id); - weaveffi_error err = {0}; - size_t out_len; - const uint8_t* result = weaveffi_products_Catalog_get_product((const weaveffi_products_Catalog*)(intptr_t)self_raw, id, &out_len, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_buffer_copy(env, out_len, result, NULL, &ret); - weaveffi_free_bytes((uint8_t*)result, out_len); - return ret; -} - -static napi_value Napi_weaveffi_products_Catalog_search(napi_env env, napi_callback_info info) { - size_t argc = 2; - napi_value args[2]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - int32_t category; - napi_get_value_int32(env, args[1], &category); - weaveffi_error err = {0}; - size_t out_len; - const uint8_t* result = weaveffi_products_Catalog_search((const weaveffi_products_Catalog*)(intptr_t)self_raw, (weaveffi_products_Category)category, &out_len, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_buffer_copy(env, out_len, result, NULL, &ret); - weaveffi_free_bytes((uint8_t*)result, out_len); - return ret; -} - -static napi_value Napi_weaveffi_products_Catalog_update_price(napi_env env, napi_callback_info info) { - size_t argc = 3; - napi_value args[3]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - int64_t id; - napi_get_value_int64(env, args[1], &id); - double price; - napi_get_value_double(env, args[2], &price); - weaveffi_error err = {0}; - bool result = weaveffi_products_Catalog_update_price((const weaveffi_products_Catalog*)(intptr_t)self_raw, id, price, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_get_boolean(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_products_Catalog_remove(napi_env env, napi_callback_info info) { - size_t argc = 2; - napi_value args[2]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - int64_t id; - napi_get_value_int64(env, args[1], &id); - weaveffi_error err = {0}; - bool result = weaveffi_products_Catalog_remove((const weaveffi_products_Catalog*)(intptr_t)self_raw, id, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_get_boolean(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_products_Catalog_destroy(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - weaveffi_products_Catalog* self = (weaveffi_products_Catalog*)(intptr_t)self_raw; - weaveffi_products_Catalog_destroy(self); - napi_value ret; - napi_get_undefined(env, &ret); - return ret; -} - -static napi_value Napi_weaveffi_orders_create_order(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - void* items_raw; - size_t items_len; - napi_get_buffer_info(env, args[0], &items_raw, &items_len); - weaveffi_error err = {0}; - int64_t result = weaveffi_orders_create_order((const uint8_t*)items_raw, items_len, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_int64(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_orders_get_order(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t id; - napi_get_value_int64(env, args[0], &id); - weaveffi_error err = {0}; - size_t out_len; - const uint8_t* result = weaveffi_orders_get_order(id, &out_len, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_buffer_copy(env, out_len, result, NULL, &ret); - weaveffi_free_bytes((uint8_t*)result, out_len); - return ret; -} - -static napi_value Napi_weaveffi_orders_cancel_order(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t id; - napi_get_value_int64(env, args[0], &id); - weaveffi_error err = {0}; - bool result = weaveffi_orders_cancel_order(id, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_get_boolean(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_orders_add_product_to_order(napi_env env, napi_callback_info info) { - size_t argc = 2; - napi_value args[2]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t order_id; - napi_get_value_int64(env, args[0], &order_id); - void* product_raw; - size_t product_len; - napi_get_buffer_info(env, args[1], &product_raw, &product_len); - weaveffi_error err = {0}; - bool result = weaveffi_orders_add_product_to_order(order_id, (const uint8_t*)product_raw, product_len, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_get_boolean(env, result, &ret); - return ret; -} - -static napi_value Init(napi_env env, napi_value exports) { - napi_property_descriptor props[] = { - { "Catalog_new", NULL, Napi_weaveffi_products_Catalog_new, NULL, NULL, NULL, napi_default, NULL }, - { "Catalog_add_product", NULL, Napi_weaveffi_products_Catalog_add_product, NULL, NULL, NULL, napi_default, NULL }, - { "Catalog_get_product", NULL, Napi_weaveffi_products_Catalog_get_product, NULL, NULL, NULL, napi_default, NULL }, - { "Catalog_search", NULL, Napi_weaveffi_products_Catalog_search, NULL, NULL, NULL, napi_default, NULL }, - { "Catalog_update_price", NULL, Napi_weaveffi_products_Catalog_update_price, NULL, NULL, NULL, napi_default, NULL }, - { "Catalog_remove", NULL, Napi_weaveffi_products_Catalog_remove, NULL, NULL, NULL, napi_default, NULL }, - { "Catalog_destroy", NULL, Napi_weaveffi_products_Catalog_destroy, NULL, NULL, NULL, napi_default, NULL }, - { "createOrder", NULL, Napi_weaveffi_orders_create_order, NULL, NULL, NULL, napi_default, NULL }, - { "getOrder", NULL, Napi_weaveffi_orders_get_order, NULL, NULL, NULL, napi_default, NULL }, - { "cancelOrder", NULL, Napi_weaveffi_orders_cancel_order, NULL, NULL, NULL, napi_default, NULL }, - { "addProductToOrder", NULL, Napi_weaveffi_orders_add_product_to_order, NULL, NULL, NULL, napi_default, NULL }, - }; - napi_define_properties(env, exports, 11, props); - return exports; -} - -NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) - -// END weaveffi_addon.c diff --git a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__weaveffi_addon_c.snap b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__weaveffi_addon_c.snap index 0806b12c..cae5e49f 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__weaveffi_addon_c.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_kitchen_sink__weaveffi_addon_c.snap @@ -7,6 +7,7 @@ source: crates/weaveffi-cli/tests/snapshots.rs #include #include "weaveffi.h" +#include #include #include @@ -743,6 +744,15 @@ static napi_value Napi_weaveffi_kitchen_nested_hello(napi_env env, napi_callback } static napi_value Init(napi_env env, napi_value exports) { + { + uint32_t found = weaveffi_abi_version(); + if (found != WEAVEFFI_ABI_VERSION) { + char msg[160]; + snprintf(msg, sizeof msg, "WeaveFFI ABI mismatch: these bindings expect revision %u but the loaded library reports revision %u", (unsigned)WEAVEFFI_ABI_VERSION, (unsigned)found); + napi_throw_error(env, NULL, msg); + return NULL; + } + } napi_property_descriptor props[] = { { "ping", NULL, Napi_weaveffi_shared_ping, NULL, NULL, NULL, napi_default, NULL }, { "Gadget_new", NULL, Napi_weaveffi_kitchen_Gadget_new, NULL, NULL, NULL, napi_default, NULL }, diff --git a/crates/weaveffi-cli/tests/snapshots/node_kvstore__binding_gyp.snap b/crates/weaveffi-cli/tests/snapshots/node_kvstore__binding_gyp.snap deleted file mode 100644 index 3997b5bb..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_kvstore__binding_gyp.snap +++ /dev/null @@ -1,19 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -{ - "targets": [ - { - "target_name": "weaveffi", - "sources": ["weaveffi_addon.c"], - "include_dirs": ["../c"], - "libraries": ["-lweaveffi"] - } - ] -} - -# END binding.gyp diff --git a/crates/weaveffi-cli/tests/snapshots/node_kvstore__index_js.snap b/crates/weaveffi-cli/tests/snapshots/node_kvstore__index_js.snap deleted file mode 100644 index 509683e4..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_kvstore__index_js.snap +++ /dev/null @@ -1,355 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// The WEAVEFFI_ADDON environment variable overrides the addon location -// (an absolute path to the built .node file); otherwise prefer the -// default node-gyp output path and fall back to a prebuilt index.node -// placed next to this file. -let addon; -if (process.env.WEAVEFFI_ADDON) { - addon = require(process.env.WEAVEFFI_ADDON); -} else { - try { - addon = require('./build/Release/weaveffi.node'); - } catch (e) { - addon = require('./index.node'); - } -} - -// Re-export every native binding, then layer the idiomatic wrappers -// (error classes, interface classes, buffer pack/unpack, function -// wrappers) on top. -const wv = {}; -for (const _name of Object.getOwnPropertyNames(addon)) { - wv[_name] = addon[_name]; -} - -class WeaveFFIError extends Error { - constructor(code, message) { - super('(' + code + ') ' + (message || '')); - this.name = 'WeaveFFIError'; - this.code = code; - this.errorMessage = message || ''; - } -} -wv.WeaveFFIError = WeaveFFIError; -function __generic(code, message) { - return new WeaveFFIError(code, message); -} -function __rebrand(e, map) { - return e && typeof e.code === 'number' ? map(e.code, e.message, e.payload) : e; -} -function __invoke(fn, args, map) { - try { - return fn.apply(null, args); - } catch (e) { - throw __rebrand(e, map); - } -} -function __invokeAsync(fn, args, map) { - return fn.apply(null, args).catch((e) => { - throw __rebrand(e, map); - }); -} - -// --- Private value-buffer runtime (WeaveFFI wire format) -------------------- -// Little-endian, packed, no alignment. Decoders reject truncated buffers, -// invalid bool/flag bytes, hostile length prefixes, and trailing bytes. -const __utf8 = new TextDecoder('utf-8', { fatal: true }); -function __bufferError(what) { - return new WeaveFFIError(-2, 'malformed value buffer: ' + what); -} -class __Writer { - constructor() { - this._buf = Buffer.alloc(256); - this._len = 0; - } - _reserve(n) { - if (this._len + n <= this._buf.length) return; - let cap = this._buf.length; - while (cap < this._len + n) cap *= 2; - const grown = Buffer.alloc(cap); - this._buf.copy(grown, 0, 0, this._len); - this._buf = grown; - } - bool(v) { this._reserve(1); this._buf[this._len++] = v ? 1 : 0; } - i8(v) { this._reserve(1); this._buf.writeInt8(v, this._len); this._len += 1; } - u8(v) { this._reserve(1); this._buf.writeUInt8(v, this._len); this._len += 1; } - i16(v) { this._reserve(2); this._buf.writeInt16LE(v, this._len); this._len += 2; } - u16(v) { this._reserve(2); this._buf.writeUInt16LE(v, this._len); this._len += 2; } - i32(v) { this._reserve(4); this._buf.writeInt32LE(v, this._len); this._len += 4; } - u32(v) { this._reserve(4); this._buf.writeUInt32LE(v, this._len); this._len += 4; } - i64(v) { this._reserve(8); this._buf.writeBigInt64LE(BigInt(v), this._len); this._len += 8; } - u64(v) { this._reserve(8); this._buf.writeBigUInt64LE(BigInt(v), this._len); this._len += 8; } - f32(v) { this._reserve(4); this._buf.writeFloatLE(v, this._len); this._len += 4; } - f64(v) { this._reserve(8); this._buf.writeDoubleLE(v, this._len); this._len += 8; } - str(v) { - const b = Buffer.from(String(v), 'utf8'); - this.u32(b.length); - this._reserve(b.length); - b.copy(this._buf, this._len); - this._len += b.length; - } - bytes(v) { - this.u32(v.length); - this._reserve(v.length); - this._buf.set(v, this._len); - this._len += v.length; - } - finish() { return this._buf.subarray(0, this._len); } -} -class __Reader { - constructor(buf) { - this._buf = Buffer.isBuffer(buf) ? buf : Buffer.from(buf); - this._pos = 0; - } - _take(n, what) { - if (this._pos + n > this._buf.length) throw __bufferError(what); - const at = this._pos; - this._pos += n; - return at; - } - bool() { - const b = this._buf[this._take(1, 'bool')]; - if (b > 1) throw __bufferError('bool byte out of range'); - return b === 1; - } - i8() { return this._buf.readInt8(this._take(1, 'i8')); } - u8() { return this._buf.readUInt8(this._take(1, 'u8')); } - i16() { return this._buf.readInt16LE(this._take(2, 'i16')); } - u16() { return this._buf.readUInt16LE(this._take(2, 'u16')); } - i32() { return this._buf.readInt32LE(this._take(4, 'i32')); } - u32() { return this._buf.readUInt32LE(this._take(4, 'u32')); } - i64() { return this._buf.readBigInt64LE(this._take(8, 'i64')); } - u64() { return this._buf.readBigUInt64LE(this._take(8, 'u64')); } - f32() { return this._buf.readFloatLE(this._take(4, 'f32')); } - f64() { return this._buf.readDoubleLE(this._take(8, 'f64')); } - len() { - const n = this.u32(); - if (n > this._buf.length - this._pos) throw __bufferError('length prefix exceeds remaining bytes'); - return n; - } - str() { - const n = this.len(); - const at = this._take(n, 'string bytes'); - try { - return __utf8.decode(this._buf.subarray(at, at + n)); - } catch (e) { - throw __bufferError('string is not valid UTF-8'); - } - } - bytes() { - const n = this.len(); - const at = this._take(n, 'byte buffer'); - return Buffer.from(this._buf.subarray(at, at + n)); - } - end() { - if (this._pos !== this._buf.length) throw __bufferError('trailing bytes after value'); - } -} -function __encode(f, v) { const w = new __Writer(); f(w, v); return w.finish(); } -function __decode(f, b) { const r = new __Reader(b); const v = f(r); r.end(); return v; } -function __wOpt(w, v, f) { if (v === null || v === undefined) { w.bool(false); } else { w.bool(true); f(w, v); } } -function __rOpt(r, f) { return r.bool() ? f(r) : null; } -function __wList(w, v, f) { w.u32(v.length); for (const e of v) f(w, e); } -function __rList(r, f) { - const n = r.len(); - const out = []; - for (let i = 0; i < n; i++) out.push(f(r)); - return out; -} -function __wMap(w, v, kf, vf) { - const keys = Object.keys(v); - w.u32(keys.length); - for (const k of keys) { kf(w, k); vf(w, v[k]); } -} -function __rMap(r, kf, vf) { - const n = r.len(); - const out = {}; - for (let i = 0; i < n; i++) { - const k = kf(r); - out[k] = vf(r); - } - return out; -} - -function __packEntry(w, v) { - w.i64(v.id); - w.str(v.key); - w.bytes(v.value); - w.i64(v.created_at); - __wOpt(w, v.expires_at, (w, v) => w.i64(v)); - __wList(w, v.tags, (w, v) => w.str(v)); - __wMap(w, v.metadata, (w, k) => w.str(k), (w, v) => w.str(v)); -} -function __unpackEntry(r) { - return { - id: Number(r.i64()), - key: r.str(), - value: r.bytes(), - created_at: Number(r.i64()), - expires_at: __rOpt(r, (r) => Number(r.i64())), - tags: __rList(r, (r) => r.str()), - metadata: __rMap(r, (r) => r.str(), (r) => r.str()), - }; -} - -function __packStats(w, v) { - w.i64(v.total_entries); - w.i64(v.total_bytes); - w.i64(v.expired_entries); -} -function __unpackStats(r) { - return { - total_entries: Number(r.i64()), - total_bytes: Number(r.i64()), - expired_entries: Number(r.i64()), - }; -} - -// Lazy iterator over a native producer: one native `next` per step. -// The native handle is released on exhaustion, by `return()` on early -// exit, or by the external's finalizer if the iterator is abandoned. -class WeaveFFIIterator { - constructor(ext, nextFn, destroyFn, map, wrapElem) { - this._ext = ext; - this._nextFn = nextFn; - this._destroyFn = destroyFn; - this._map = map; - this._wrapElem = wrapElem; - this._done = false; - } - next() { - if (this._done) { - return { done: true, value: undefined }; - } - const _v = __invoke(this._nextFn, [this._ext], this._map); - if (_v === undefined) { - this._done = true; - return { done: true, value: undefined }; - } - return { done: false, value: this._wrapElem ? this._wrapElem(_v) : _v }; - } - return(value) { - if (!this._done) { - this._done = true; - this._destroyFn(this._ext); - } - return { done: true, value }; - } - [Symbol.iterator]() { - return this; - } -} - -class KvError extends WeaveFFIError { - constructor(code, message) { - super(code, message); - this.name = 'KvError'; - } -} -wv.KvError = KvError; -class KeyNotFoundError extends KvError { - constructor(message) { - super(1001, message || 'key not found'); - this.name = 'KeyNotFoundError'; - } -} -KeyNotFoundError.CODE = 1001; -wv.KeyNotFoundError = KeyNotFoundError; -class ExpiredError extends KvError { - constructor(message) { - super(1002, message || 'entry expired'); - this.name = 'ExpiredError'; - } -} -ExpiredError.CODE = 1002; -wv.ExpiredError = ExpiredError; -class StoreFullError extends KvError { - constructor(message) { - super(1003, message || 'store has reached capacity'); - this.name = 'StoreFullError'; - } -} -StoreFullError.CODE = 1003; -wv.StoreFullError = StoreFullError; -class IoError extends KvError { - constructor(message) { - super(1004, message || 'I/O failure'); - this.name = 'IoError'; - } -} -IoError.CODE = 1004; -wv.IoError = IoError; -const __kvErrorCodes = Object.freeze({ 1001: KeyNotFoundError, 1002: ExpiredError, 1003: StoreFullError, 1004: IoError }); -function __kvErrorFrom(code, message, payload) { - const _cls = __kvErrorCodes[code]; - const _err = _cls === undefined ? new WeaveFFIError(code, message) : new _cls(message); - return _err; -} - -class Store { - static open(path) { - const _r = __invoke(addon.Store_open, [path], __kvErrorFrom); - return Store._fromHandle(_r); - } - put(key, value, kind, ttlSeconds) { - return __invoke(addon.Store_put, [this._handle, key, value, kind, __encode((w, v) => __wOpt(w, v, (w, v) => w.i64(v)), ttlSeconds)], __kvErrorFrom); - } - get(key) { - const _r = __invoke(addon.Store_get, [this._handle, key], __kvErrorFrom); - return __decode((r) => __rOpt(r, __unpackEntry), _r); - } - delete(key) { - return __invoke(addon.Store_delete, [this._handle, key], __kvErrorFrom); - } - listKeys(prefix) { - const _it = __invoke(addon.Store_list_keys, [this._handle, __encode((w, v) => __wOpt(w, v, (w, v) => w.str(v)), prefix)], __kvErrorFrom); - return new WeaveFFIIterator(_it, addon.Store_list_keys_iterNext, addon.Store_list_keys_iterDestroy, __kvErrorFrom, null); - } - count() { - return __invoke(addon.Store_count, [this._handle], __generic); - } - clear() { - return __invoke(addon.Store_clear, [this._handle], __generic); - } - compact() { - return __invokeAsync(addon.Store_compact, [this._handle], __kvErrorFrom); - } - legacyPut(key, value) { - return __invoke(addon.Store_legacy_put, [this._handle, key, value], __kvErrorFrom); - } - static defaultCapacity() { - return __invoke(addon.Store_default_capacity, [], __generic); - } - destroy() { - if (this._handle) { - Store._cleanup.unregister(this); - addon.Store_destroy(this._handle); - this._handle = 0; - } - } -} -Store._fromHandle = function (handle) { - const _o = Object.create(Store.prototype); - _o._handle = handle; - Store._cleanup.register(_o, handle, _o); - return _o; -}; -Store._cleanup = new FinalizationRegistry((handle) => { - if (handle) { addon.Store_destroy(handle); } -}); -wv.Store = Store; - -wv.getStats = function (store) { - const _r = __invoke(addon.getStats, [store instanceof Store ? store._handle : store], __kvErrorFrom); - return __decode(__unpackStats, _r); -}; - -module.exports = wv; - -// END index.js diff --git a/crates/weaveffi-cli/tests/snapshots/node_kvstore__package_json.snap b/crates/weaveffi-cli/tests/snapshots/node_kvstore__package_json.snap deleted file mode 100644 index 6fd366d2..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_kvstore__package_json.snap +++ /dev/null @@ -1,17 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -{ - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", - "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", - "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", - "main": "index.js", - "types": "types.d.ts", - "gypfile": true, - "scripts": { - "install": "node-gyp rebuild" - } -} diff --git a/crates/weaveffi-cli/tests/snapshots/node_kvstore__types_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/node_kvstore__types_d_ts.snap deleted file mode 100644 index 7bf6c740..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_kvstore__types_d_ts.snap +++ /dev/null @@ -1,155 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// Generated types for WeaveFFI functions -/** - * Base class of every error thrown by these bindings. Non-throwing - * functions reject or throw it directly for panics and marshalling - * failures; throwing functions surface a module domain subclass. - */ -export class WeaveFFIError extends Error { - /** The numeric ABI error code. */ - code: number; - /** The raw producer message, without the code prefix. */ - errorMessage: string; - constructor(code: number, message: string); -} -/** Typed errors reported by the `kv` module's throwing functions. */ -export class KvError extends WeaveFFIError { - constructor(code: number, message: string); -} -/** No entry exists for the requested key */ -export class KeyNotFoundError extends KvError { - static readonly CODE: 1001; - constructor(message?: string); -} -/** The entry exists but its TTL has elapsed */ -export class ExpiredError extends KvError { - static readonly CODE: 1002; - constructor(message?: string); -} -/** The store has reached its capacity limit */ -export class StoreFullError extends KvError { - static readonly CODE: 1003; - constructor(message?: string); -} -/** Underlying storage reported an I/O failure */ -export class IoError extends KvError { - static readonly CODE: 1004; - constructor(message?: string); -} -/** A single key-value entry persisted in the store */ -export interface Entry { - /** Stable monotonic identifier assigned on insert */ - id: number; - /** UTF-8 lookup key */ - key: string; - /** Opaque binary payload */ - value: Buffer; - /** Unix-timestamp seconds when the entry was created */ - created_at: number; - /** Optional unix-timestamp seconds at which the entry expires */ - expires_at: number | null; - /** Free-form labels attached to the entry */ - tags: string[]; - /** Arbitrary string-valued metadata pairs */ - metadata: Record; -} -/** Persistence semantics applied to a stored entry */ -export enum EntryKind { - /** In-memory only; lost on close */ - Volatile = 0, - /** Flushed to durable storage */ - Persistent = 1, - /** Persistent and encrypted at rest */ - Encrypted = 2, -} -// module kv -/** An in-memory key-value store owning its entries; destroying the object releases all of its state */ -export class Store { - /** - * Open (or create) a store backed by the given filesystem path - * Maps to C function: weaveffi_kv_Store_open - * @throws {KvError} - */ - static open(path: string): Store; - /** - * Insert or replace a value, returning true on success - * Maps to C function: weaveffi_kv_Store_put - * @throws {KvError} - */ - put(key: string, value: Buffer, kind: EntryKind, ttlSeconds: number | null): boolean; - /** - * Look up an entry by key; returns null if missing or expired - * Maps to C function: weaveffi_kv_Store_get - * @throws {KvError} - */ - get(key: string): Entry | null; - /** - * Remove the entry for the given key, returning true if it existed - * Maps to C function: weaveffi_kv_Store_delete - * @throws {KvError} - */ - delete(key: string): boolean; - /** - * Stream every key, optionally filtered by a prefix - * Maps to C function: weaveffi_kv_Store_list_keys - * @throws {KvError} - */ - listKeys(prefix: string | null): IterableIterator; - /** - * Return the number of live entries in the store - * Maps to C function: weaveffi_kv_Store_count - */ - count(): number; - /** - * Drop every entry from the store - * Maps to C function: weaveffi_kv_Store_clear - */ - clear(): void; - /** - * Reclaim space asynchronously; returns the number of bytes reclaimed - * Maps to C function: weaveffi_kv_Store_compact - * @throws {KvError} - */ - compact(): Promise; - /** - * Legacy single-shot put kept for compatibility - * Maps to C function: weaveffi_kv_Store_legacy_put - * @throws {KvError} - * @deprecated use put() with explicit kind - */ - legacyPut(key: string, value: Buffer): boolean; - /** - * The largest number of live entries one store will hold - * Maps to C function: weaveffi_kv_Store_default_capacity - */ - static defaultCapacity(): number; - /** Free the underlying native object. */ - destroy(): void; -} -/** Subscribe to per-key eviction notifications */ -export function registerEvictionListener(callback: (key: string) => void): number -export function unregisterEvictionListener(id: number): void -/** Aggregate store statistics */ -export interface Stats { - /** Number of live entries */ - total_entries: number; - /** Sum of all value byte lengths */ - total_bytes: number; - /** Number of entries past their TTL but not yet evicted */ - expired_entries: number; -} -// module kv_stats -/** - * Snapshot the current store statistics - * Maps to C function: weaveffi_kv_stats_get_stats - * @throws {KvError} - */ -export function getStats(store: Store): Stats - -// END types.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/node_kvstore__weaveffi_addon_c.snap b/crates/weaveffi-cli/tests/snapshots/node_kvstore__weaveffi_addon_c.snap deleted file mode 100644 index 76c7d872..00000000 --- a/crates/weaveffi-cli/tests/snapshots/node_kvstore__weaveffi_addon_c.snap +++ /dev/null @@ -1,483 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -#include -#include "weaveffi.h" -#include -#include - -static napi_value weaveffi_napi_error_value(napi_env env, int32_t code, const char* message, const uint8_t* payload_ptr, size_t payload_len) { - napi_value msg; - napi_create_string_utf8(env, message ? message : "", NAPI_AUTO_LENGTH, &msg); - napi_value err; - napi_create_error(env, NULL, msg, &err); - napi_value code_val; - napi_create_int32(env, code, &code_val); - napi_set_named_property(env, err, "code", code_val); - if (payload_ptr != NULL) { - napi_value payload_val; - napi_create_buffer_copy(env, payload_len, payload_ptr, NULL, &payload_val); - napi_set_named_property(env, err, "payload", payload_val); - } - return err; -} - -typedef struct { - void* iter; -} weaveffi_napi_iter_state; - -typedef struct weaveffi_napi_listener_ctx { - napi_threadsafe_function tsfn; - uint64_t id; - struct weaveffi_napi_listener_ctx* next; -} weaveffi_napi_listener_ctx; - -static weaveffi_napi_listener_ctx* weaveffi_napi_listeners = NULL; - -static napi_value Napi_weaveffi_kv_Store_open(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - size_t path_len; - napi_get_value_string_utf8(env, args[0], NULL, 0, &path_len); - char* path = (char*)malloc(path_len + 1); - napi_get_value_string_utf8(env, args[0], path, path_len + 1, &path_len); - weaveffi_error err = {0}; - weaveffi_kv_Store* result = weaveffi_kv_Store_open(path, &err); - free(path); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_int64(env, (int64_t)(intptr_t)result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kv_Store_default_capacity(napi_env env, napi_callback_info info) { - size_t argc = 0; - napi_get_cb_info(env, info, &argc, NULL, NULL, NULL); - weaveffi_error err = {0}; - int64_t result = weaveffi_kv_Store_default_capacity(&err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_int64(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kv_Store_put(napi_env env, napi_callback_info info) { - size_t argc = 5; - napi_value args[5]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - size_t key_len; - napi_get_value_string_utf8(env, args[1], NULL, 0, &key_len); - char* key = (char*)malloc(key_len + 1); - napi_get_value_string_utf8(env, args[1], key, key_len + 1, &key_len); - void* value_raw; - size_t value_len; - napi_get_buffer_info(env, args[2], &value_raw, &value_len); - int32_t kind; - napi_get_value_int32(env, args[3], &kind); - void* ttl_seconds_raw; - size_t ttl_seconds_len; - napi_get_buffer_info(env, args[4], &ttl_seconds_raw, &ttl_seconds_len); - weaveffi_error err = {0}; - bool result = weaveffi_kv_Store_put((const weaveffi_kv_Store*)(intptr_t)self_raw, key, (const uint8_t*)value_raw, value_len, (weaveffi_kv_EntryKind)kind, (const uint8_t*)ttl_seconds_raw, ttl_seconds_len, &err); - free(key); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_get_boolean(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kv_Store_get(napi_env env, napi_callback_info info) { - size_t argc = 2; - napi_value args[2]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - size_t key_len; - napi_get_value_string_utf8(env, args[1], NULL, 0, &key_len); - char* key = (char*)malloc(key_len + 1); - napi_get_value_string_utf8(env, args[1], key, key_len + 1, &key_len); - weaveffi_error err = {0}; - size_t out_len; - const uint8_t* result = weaveffi_kv_Store_get((const weaveffi_kv_Store*)(intptr_t)self_raw, key, &out_len, &err); - free(key); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_buffer_copy(env, out_len, result, NULL, &ret); - weaveffi_free_bytes((uint8_t*)result, out_len); - return ret; -} - -static napi_value Napi_weaveffi_kv_Store_delete(napi_env env, napi_callback_info info) { - size_t argc = 2; - napi_value args[2]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - size_t key_len; - napi_get_value_string_utf8(env, args[1], NULL, 0, &key_len); - char* key = (char*)malloc(key_len + 1); - napi_get_value_string_utf8(env, args[1], key, key_len + 1, &key_len); - weaveffi_error err = {0}; - bool result = weaveffi_kv_Store_delete((const weaveffi_kv_Store*)(intptr_t)self_raw, key, &err); - free(key); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_get_boolean(env, result, &ret); - return ret; -} - -static void weaveffi_kv_Store_list_keys_napi_iter_finalize(napi_env env, void* data, void* hint) { - (void)env; - (void)hint; - weaveffi_napi_iter_state* state = (weaveffi_napi_iter_state*)data; - if (state->iter != NULL) { - weaveffi_kv_Store_ListKeysIterator_destroy((weaveffi_kv_Store_ListKeysIterator*)state->iter); - state->iter = NULL; - } - free(state); -} - -static napi_value Napi_weaveffi_kv_Store_ListKeysIterator_next(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - void* iter_data = NULL; - napi_get_value_external(env, args[0], &iter_data); - weaveffi_napi_iter_state* state = (weaveffi_napi_iter_state*)iter_data; - napi_value ret; - if (state == NULL || state->iter == NULL) { - napi_get_undefined(env, &ret); - return ret; - } - const char* iter_item; - weaveffi_error iter_err = {0}; - if (!weaveffi_kv_Store_ListKeysIterator_next((weaveffi_kv_Store_ListKeysIterator*)state->iter, &iter_item, &iter_err)) { - weaveffi_kv_Store_ListKeysIterator_destroy((weaveffi_kv_Store_ListKeysIterator*)state->iter); - state->iter = NULL; - if (iter_err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, iter_err.code, iter_err.message, iter_err.payload_ptr, iter_err.payload_len)); - weaveffi_error_clear(&iter_err); - return NULL; - } - napi_get_undefined(env, &ret); - return ret; - } - napi_create_string_utf8(env, iter_item ? iter_item : "", NAPI_AUTO_LENGTH, &ret); - weaveffi_free_string((char*)iter_item); - return ret; -} - -static napi_value Napi_weaveffi_kv_Store_ListKeysIterator_destroy(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - void* iter_data = NULL; - napi_get_value_external(env, args[0], &iter_data); - weaveffi_napi_iter_state* state = (weaveffi_napi_iter_state*)iter_data; - if (state != NULL && state->iter != NULL) { - weaveffi_kv_Store_ListKeysIterator_destroy((weaveffi_kv_Store_ListKeysIterator*)state->iter); - state->iter = NULL; - } - napi_value ret; - napi_get_undefined(env, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kv_Store_list_keys(napi_env env, napi_callback_info info) { - size_t argc = 2; - napi_value args[2]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - void* prefix_raw; - size_t prefix_len; - napi_get_buffer_info(env, args[1], &prefix_raw, &prefix_len); - weaveffi_error err = {0}; - void* result = weaveffi_kv_Store_list_keys((const weaveffi_kv_Store*)(intptr_t)self_raw, (const uint8_t*)prefix_raw, prefix_len, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - weaveffi_napi_iter_state* iter_state = (weaveffi_napi_iter_state*)calloc(1, sizeof(weaveffi_napi_iter_state)); - iter_state->iter = (void*)result; - napi_create_external(env, iter_state, weaveffi_kv_Store_list_keys_napi_iter_finalize, NULL, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kv_Store_count(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - weaveffi_error err = {0}; - int64_t result = weaveffi_kv_Store_count((const weaveffi_kv_Store*)(intptr_t)self_raw, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_int64(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kv_Store_clear(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - weaveffi_error err = {0}; - weaveffi_kv_Store_clear((const weaveffi_kv_Store*)(intptr_t)self_raw, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_get_undefined(env, &ret); - return ret; -} - -typedef struct { - napi_deferred deferred; - napi_threadsafe_function tsfn; - int32_t err_code; - char* err_msg; - uint8_t* err_payload; - size_t err_payload_len; - int64_t result; -} weaveffi_kv_Store_compact_napi_actx; - -static void weaveffi_kv_Store_compact_napi_cb(void* context, weaveffi_error* err, int64_t result) { - weaveffi_kv_Store_compact_napi_actx* ctx = (weaveffi_kv_Store_compact_napi_actx*)context; - if (err != NULL && err->code != 0) { - ctx->err_code = err->code; - ctx->err_msg = err->message ? strdup(err->message) : strdup("unknown error"); - ctx->err_payload_len = err->payload_len; - if (err->payload_ptr != NULL && err->payload_len > 0) { ctx->err_payload = (uint8_t*)malloc(err->payload_len); memcpy(ctx->err_payload, err->payload_ptr, err->payload_len); } - } else { - ctx->result = result; - } - weaveffi_error_free(err); - napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking); -} - -static void weaveffi_kv_Store_compact_napi_settle(napi_env env, napi_value js_cb, void* context, void* data) { - (void)js_cb; - (void)context; - weaveffi_kv_Store_compact_napi_actx* ctx = (weaveffi_kv_Store_compact_napi_actx*)data; - if (env != NULL) { - if (ctx->err_code != 0) { - napi_value err_obj = weaveffi_napi_error_value(env, ctx->err_code, ctx->err_msg, ctx->err_payload, ctx->err_payload_len); - napi_reject_deferred(env, ctx->deferred, err_obj); - } else { - napi_value val; - napi_create_int64(env, ctx->result, &val); - napi_resolve_deferred(env, ctx->deferred, val); - } - } - free(ctx->err_msg); - free(ctx->err_payload); - napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release); - free(ctx); -} - -static napi_value Napi_weaveffi_kv_Store_compact(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - weaveffi_kv_Store_compact_napi_actx* ctx = (weaveffi_kv_Store_compact_napi_actx*)calloc(1, sizeof(weaveffi_kv_Store_compact_napi_actx)); - napi_value promise; - napi_create_promise(env, &ctx->deferred, &promise); - napi_value resource_name; - napi_create_string_utf8(env, "weaveffi_kv_Store_compact", NAPI_AUTO_LENGTH, &resource_name); - napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_kv_Store_compact_napi_settle, &ctx->tsfn); - weaveffi_kv_Store_compact_async((const weaveffi_kv_Store*)(intptr_t)self_raw, NULL, weaveffi_kv_Store_compact_napi_cb, ctx); - return promise; -} - -static napi_value Napi_weaveffi_kv_Store_legacy_put(napi_env env, napi_callback_info info) { - size_t argc = 3; - napi_value args[3]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - size_t key_len; - napi_get_value_string_utf8(env, args[1], NULL, 0, &key_len); - char* key = (char*)malloc(key_len + 1); - napi_get_value_string_utf8(env, args[1], key, key_len + 1, &key_len); - void* value_raw; - size_t value_len; - napi_get_buffer_info(env, args[2], &value_raw, &value_len); - weaveffi_error err = {0}; - bool result = weaveffi_kv_Store_legacy_put((const weaveffi_kv_Store*)(intptr_t)self_raw, key, (const uint8_t*)value_raw, value_len, &err); - free(key); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_get_boolean(env, result, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kv_Store_destroy(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t self_raw; - napi_get_value_int64(env, args[0], &self_raw); - weaveffi_kv_Store* self = (weaveffi_kv_Store*)(intptr_t)self_raw; - weaveffi_kv_Store_destroy(self); - napi_value ret; - napi_get_undefined(env, &ret); - return ret; -} - -typedef struct { - char* key; -} weaveffi_kv_OnEvict_fn_payload; - -static void weaveffi_kv_OnEvict_fn_napi_tramp(const char* key, void* context) { - weaveffi_napi_listener_ctx* ctx = (weaveffi_napi_listener_ctx*)context; - weaveffi_kv_OnEvict_fn_payload* p = (weaveffi_kv_OnEvict_fn_payload*)calloc(1, sizeof(weaveffi_kv_OnEvict_fn_payload)); - p->key = key ? strdup(key) : NULL; - napi_call_threadsafe_function(ctx->tsfn, p, napi_tsfn_nonblocking); -} - -static void weaveffi_kv_OnEvict_fn_napi_calljs(napi_env env, napi_value js_cb, void* context, void* data) { - (void)context; - weaveffi_kv_OnEvict_fn_payload* p = (weaveffi_kv_OnEvict_fn_payload*)data; - if (env != NULL) { - napi_value undefined; - napi_get_undefined(env, &undefined); - napi_value argv[1]; - napi_create_string_utf8(env, p->key ? p->key : "", NAPI_AUTO_LENGTH, &argv[0]); - napi_call_function(env, undefined, js_cb, 1, argv, NULL); - } - free(p->key); - free(p); -} - -static napi_value Napi_weaveffi_kv_register_eviction_listener(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - weaveffi_napi_listener_ctx* ctx = (weaveffi_napi_listener_ctx*)calloc(1, sizeof(weaveffi_napi_listener_ctx)); - napi_value resource_name; - napi_create_string_utf8(env, "weaveffi_kv_register_eviction_listener", NAPI_AUTO_LENGTH, &resource_name); - napi_create_threadsafe_function(env, args[0], NULL, resource_name, 0, 1, NULL, NULL, NULL, weaveffi_kv_OnEvict_fn_napi_calljs, &ctx->tsfn); - napi_unref_threadsafe_function(env, ctx->tsfn); - uint64_t id = weaveffi_kv_register_eviction_listener(weaveffi_kv_OnEvict_fn_napi_tramp, ctx); - ctx->id = id; - ctx->next = weaveffi_napi_listeners; - weaveffi_napi_listeners = ctx; - napi_value ret; - napi_create_double(env, (double)id, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kv_unregister_eviction_listener(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - double id_d = 0; - napi_get_value_double(env, args[0], &id_d); - uint64_t id = (uint64_t)id_d; - weaveffi_kv_unregister_eviction_listener(id); - weaveffi_napi_listener_ctx** link = &weaveffi_napi_listeners; - while (*link != NULL) { - if ((*link)->id == id) { - weaveffi_napi_listener_ctx* found = *link; - *link = found->next; - napi_release_threadsafe_function(found->tsfn, napi_tsfn_release); - free(found); - break; - } - link = &(*link)->next; - } - napi_value ret; - napi_get_undefined(env, &ret); - return ret; -} - -static napi_value Napi_weaveffi_kv_stats_get_stats(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1]; - napi_get_cb_info(env, info, &argc, args, NULL, NULL); - int64_t store_raw; - napi_get_value_int64(env, args[0], &store_raw); - weaveffi_error err = {0}; - size_t out_len; - const uint8_t* result = weaveffi_kv_stats_get_stats((const weaveffi_kv_Store*)(intptr_t)store_raw, &out_len, &err); - if (err.code != 0) { - napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len)); - weaveffi_error_clear(&err); - return NULL; - } - napi_value ret; - napi_create_buffer_copy(env, out_len, result, NULL, &ret); - weaveffi_free_bytes((uint8_t*)result, out_len); - return ret; -} - -static napi_value Init(napi_env env, napi_value exports) { - napi_property_descriptor props[] = { - { "Store_open", NULL, Napi_weaveffi_kv_Store_open, NULL, NULL, NULL, napi_default, NULL }, - { "Store_default_capacity", NULL, Napi_weaveffi_kv_Store_default_capacity, NULL, NULL, NULL, napi_default, NULL }, - { "Store_put", NULL, Napi_weaveffi_kv_Store_put, NULL, NULL, NULL, napi_default, NULL }, - { "Store_get", NULL, Napi_weaveffi_kv_Store_get, NULL, NULL, NULL, napi_default, NULL }, - { "Store_delete", NULL, Napi_weaveffi_kv_Store_delete, NULL, NULL, NULL, napi_default, NULL }, - { "Store_list_keys_iterNext", NULL, Napi_weaveffi_kv_Store_ListKeysIterator_next, NULL, NULL, NULL, napi_default, NULL }, - { "Store_list_keys_iterDestroy", NULL, Napi_weaveffi_kv_Store_ListKeysIterator_destroy, NULL, NULL, NULL, napi_default, NULL }, - { "Store_list_keys", NULL, Napi_weaveffi_kv_Store_list_keys, NULL, NULL, NULL, napi_default, NULL }, - { "Store_count", NULL, Napi_weaveffi_kv_Store_count, NULL, NULL, NULL, napi_default, NULL }, - { "Store_clear", NULL, Napi_weaveffi_kv_Store_clear, NULL, NULL, NULL, napi_default, NULL }, - { "Store_compact", NULL, Napi_weaveffi_kv_Store_compact, NULL, NULL, NULL, napi_default, NULL }, - { "Store_legacy_put", NULL, Napi_weaveffi_kv_Store_legacy_put, NULL, NULL, NULL, napi_default, NULL }, - { "Store_destroy", NULL, Napi_weaveffi_kv_Store_destroy, NULL, NULL, NULL, napi_default, NULL }, - { "registerEvictionListener", NULL, Napi_weaveffi_kv_register_eviction_listener, NULL, NULL, NULL, napi_default, NULL }, - { "unregisterEvictionListener", NULL, Napi_weaveffi_kv_unregister_eviction_listener, NULL, NULL, NULL, napi_default, NULL }, - { "getStats", NULL, Napi_weaveffi_kv_stats_get_stats, NULL, NULL, NULL, napi_default, NULL }, - }; - napi_define_properties(env, exports, 16, props); - return exports; -} - -NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) - -// END weaveffi_addon.c diff --git a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__weaveffi_addon_c.snap b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__weaveffi_addon_c.snap index 7ebe9a9a..1bfc3bc7 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_nested_modules__weaveffi_addon_c.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_nested_modules__weaveffi_addon_c.snap @@ -7,6 +7,7 @@ source: crates/weaveffi-cli/tests/snapshots.rs #include #include "weaveffi.h" +#include #include #include @@ -105,6 +106,15 @@ static napi_value Napi_weaveffi_graphics_shapes_geometry_compute(napi_env env, n } static napi_value Init(napi_env env, napi_value exports) { + { + uint32_t found = weaveffi_abi_version(); + if (found != WEAVEFFI_ABI_VERSION) { + char msg[160]; + snprintf(msg, sizeof msg, "WeaveFFI ABI mismatch: these bindings expect revision %u but the loaded library reports revision %u", (unsigned)WEAVEFFI_ABI_VERSION, (unsigned)found); + napi_throw_error(env, NULL, msg); + return NULL; + } + } napi_property_descriptor props[] = { { "newCanvas", NULL, Napi_weaveffi_graphics_new_canvas, NULL, NULL, NULL, napi_default, NULL }, { "makeShape", NULL, Napi_weaveffi_graphics_shapes_make_shape, NULL, NULL, NULL, napi_default, NULL }, diff --git a/crates/weaveffi-cli/tests/snapshots/node_shapes__weaveffi_addon_c.snap b/crates/weaveffi-cli/tests/snapshots/node_shapes__weaveffi_addon_c.snap index 99ac30b6..d1390958 100644 --- a/crates/weaveffi-cli/tests/snapshots/node_shapes__weaveffi_addon_c.snap +++ b/crates/weaveffi-cli/tests/snapshots/node_shapes__weaveffi_addon_c.snap @@ -7,6 +7,7 @@ source: crates/weaveffi-cli/tests/snapshots.rs #include #include "weaveffi.h" +#include #include #include @@ -127,6 +128,15 @@ static napi_value Napi_weaveffi_shapes_make_numerics(napi_env env, napi_callback } static napi_value Init(napi_env env, napi_value exports) { + { + uint32_t found = weaveffi_abi_version(); + if (found != WEAVEFFI_ABI_VERSION) { + char msg[160]; + snprintf(msg, sizeof msg, "WeaveFFI ABI mismatch: these bindings expect revision %u but the loaded library reports revision %u", (unsigned)WEAVEFFI_ABI_VERSION, (unsigned)found); + napi_throw_error(env, NULL, msg); + return NULL; + } + } napi_property_descriptor props[] = { { "describe", NULL, Napi_weaveffi_shapes_describe, NULL, NULL, NULL, napi_default, NULL }, { "scale", NULL, Napi_weaveffi_shapes_scale, NULL, NULL, NULL, napi_default, NULL }, diff --git a/crates/weaveffi-cli/tests/snapshots/python_async_demo__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_async_demo__weaveffi_weaveffi_py.snap deleted file mode 100644 index 38680aee..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_async_demo__weaveffi_weaveffi_py.snap +++ /dev/null @@ -1,516 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -"""WeaveFFI Python ctypes bindings (auto-generated)""" -import contextlib -import ctypes -import os -import platform -import struct -from dataclasses import dataclass -from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional - - -class WeaveFFIError(Exception): - def __init__(self, code: int, message: str) -> None: - self.code = code - self.message = message - super().__init__(f"({code}) {message}") - - -class _WeaveFFIErrorStruct(ctypes.Structure): - _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 in WEAVEFFI_LIBRARY wins, so callers can point at a - # specific build artifact regardless of its file name or location. - override = os.environ.get("WEAVEFFI_LIBRARY") - if override: - return ctypes.CDLL(override) - system = platform.system() - if system == "Darwin": - name = "libweaveffi.dylib" - elif system == "Windows": - name = "weaveffi.dll" - else: - name = "libweaveffi.so" - return ctypes.CDLL(name) - - -_lib = _load_library() -_lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] -_lib.weaveffi_error_clear.restype = None -# Async completion callbacks receive a heap-boxed error the consumer owns; -# weaveffi_error_free releases the message, the payload, and the box. -_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] -_lib.weaveffi_error_free.restype = None -# The free helpers take raw addresses (`c_void_p`) so wrappers can release -# owned producer allocations they hold as plain integers or typed pointers. -_lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] -_lib.weaveffi_free_string.restype = None -_lib.weaveffi_free_bytes.argtypes = [ctypes.c_void_p, ctypes.c_size_t] -_lib.weaveffi_free_bytes.restype = None - - -def _check_error(err: _WeaveFFIErrorStruct) -> None: - if err.code != 0: - code = err.code - message = err.message.decode("utf-8") if err.message else "" - _lib.weaveffi_error_clear(ctypes.byref(err)) - raise WeaveFFIError(code, message) - - -class _PointerGuard(contextlib.AbstractContextManager): - def __init__(self, ptr, free_fn) -> None: - self.ptr = ptr - self._free_fn = free_fn - - def __exit__(self, *exc) -> bool: - if self.ptr is not None: - self._free_fn(self.ptr) - self.ptr = None - return False - - -def _string_to_bytes(s: Optional[str]) -> Optional[bytes]: - if s is None: - return None - return s.encode("utf-8") - - -def _bytes_to_string(ptr) -> Optional[str]: - if ptr is None: - return None - return ptr.decode("utf-8") - - -def _take_string(ptr) -> Optional[str]: - """Copy an owned C string (a raw address) and free the producer buffer.""" - if not ptr: - return None - _s = ctypes.string_at(ptr).decode("utf-8") - _lib.weaveffi_free_string(ptr) - return _s - - -class _BufferWriter: - """Encodes values into the WeaveFFI value-buffer wire format: - little-endian, packed, no alignment.""" - - def __init__(self) -> None: - self._buf = bytearray() - - def finish(self) -> bytes: - return bytes(self._buf) - - def write_bool(self, v) -> None: - self._buf.append(1 if v else 0) - - def write_i8(self, v: int) -> None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf.append(1 if present else 0) - - def write_string(self, v: str) -> None: - _b = v.encode("utf-8") - self.write_len(len(_b)) - self._buf += _b - - def write_bytes(self, v: bytes) -> None: - self.write_len(len(v)) - self._buf += bytes(v) - - -class _BufferReader: - """Decodes values from the WeaveFFI value-buffer wire format, rejecting - truncated buffers, invalid flag bytes, invalid UTF-8, and oversized - length prefixes.""" - - def __init__(self, data: bytes) -> None: - self._data = memoryview(data) - self._pos = 0 - - def _take(self, n: int, what: str) -> memoryview: - if len(self._data) - self._pos < n: - raise WeaveFFIError(-1, f"malformed value buffer: truncated {what}") - _view = self._data[self._pos:self._pos + n] - self._pos += n - return _view - - def read_bool(self) -> bool: - _b = self._take(1, "bool")[0] - if _b > 1: - raise WeaveFFIError(-1, "malformed value buffer: invalid bool byte") - return _b == 1 - - def read_i8(self) -> int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" float: - return struct.unpack(" float: - return struct.unpack(" int: - _n = struct.unpack(" len(self._data) - self._pos: - raise WeaveFFIError( - -1, "malformed value buffer: length prefix exceeds remaining bytes" - ) - return _n - - def read_option_flag(self) -> bool: - _b = self._take(1, "option flag")[0] - if _b > 1: - raise WeaveFFIError(-1, "malformed value buffer: invalid option flag") - return _b == 1 - - def read_string(self) -> str: - _n = self.read_len() - try: - return str(self._take(_n, "string data"), "utf-8") - except UnicodeDecodeError as _e: - raise WeaveFFIError( - -1, "malformed value buffer: string is not valid UTF-8" - ) from _e - - def read_bytes(self) -> bytes: - _n = self.read_len() - return bytes(self._take(_n, "bytes data")) - - def expect_end(self) -> None: - if self._pos != len(self._data): - raise WeaveFFIError(-1, "malformed value buffer: trailing bytes") - - -def _decode_buffer(data: bytes, read_fn): - """Decode exactly one value from `data` using `read_fn(reader)`.""" - _r = _BufferReader(data) - _v = read_fn(_r) - _r.expect_end() - return _v - - -def _take_buffer(ptr, length) -> bytes: - """Copy an owned value buffer (a raw address) and release it with - weaveffi_free_bytes.""" - if not ptr: - return b"" - _data = ctypes.string_at(ptr, length) if length else b"" - _lib.weaveffi_free_bytes(ptr, ctypes.c_size_t(length)) - return _data - -import asyncio -import threading - - -# Pending async completion trampolines, keyed by an integer token. -# Holding the ctypes function objects here keeps them alive until the -# producer fires the completion callback, even when the awaiting -# coroutine has been cancelled; each entry is removed on completion. -_async_pending: Dict[int, object] = {} -_async_lock = threading.Lock() -_async_next_token = 0 - - -def _async_register(cb) -> int: - global _async_next_token - with _async_lock: - _async_next_token += 1 - _token = _async_next_token - _async_pending[_token] = cb - return _token - - -# === Module: tasks === - -class TaskError(WeaveFFIError): - """Base exception for the `tasks` module's error domain.""" - - -class InvalidName(TaskError): - """task name must not be empty""" - - CODE = 1 - - def __init__(self, message: str = "task name must not be empty") -> None: - super().__init__(1, message) - - -TaskError.InvalidName = InvalidName - - -_TASK_ERROR_CODES: Dict[int, type] = { - 1: InvalidName, -} - - -def _task_error_from(code: int, message: str, payload: bytes = b"") -> WeaveFFIError: - """Build the TaskError subclass matching `code`, or a generic - WeaveFFIError for codes outside the domain (panics, marshalling).""" - _cls = _TASK_ERROR_CODES.get(code) - if _cls is None: - return WeaveFFIError(code, message) - _exc = _cls(message) if message else _cls() - return _exc - - -def _check_task_error(err: _WeaveFFIErrorStruct) -> None: - if err.code != 0: - code = err.code - message = err.message.decode("utf-8") if err.message else "" - payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" - _lib.weaveffi_error_clear(ctypes.byref(err)) - raise _task_error_from(code, message, payload) - - -@dataclass -class TaskResult: - id: int - value: str - success: bool - - -def _write_TaskResult(_w: _BufferWriter, value: "TaskResult") -> None: - _w.write_i64(value.id) - _w.write_string(value.value) - _w.write_bool(value.success) - - -def _read_TaskResult(_r: _BufferReader) -> "TaskResult": - return TaskResult( - id=_r.read_i64(), - value=_r.read_string(), - success=_r.read_bool(), - ) - - -def _pack_TaskResult(value: "TaskResult") -> bytes: - _w = _BufferWriter() - _write_TaskResult(_w, value) - return _w.finish() - - -def _unpack_TaskResult(data: bytes) -> "TaskResult": - return _decode_buffer(data, _read_TaskResult) - - -async def run_task(name: str) -> "TaskResult": - """ - Raises - ------ - TaskError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_tasks_run_task_async - _loop = asyncio.get_running_loop() - _fut = _loop.create_future() - - def _cb_impl(context, err, result_ptr, result_len): - # Fires exactly once, on a producer thread: take ownership of - # the result here, then hop back to the event loop. - _state = {"err": None, "val": None} - if err and err.contents.code != 0: - _code = err.contents.code - _msg = err.contents.message.decode("utf-8") if err.contents.message else "" - _payload = ctypes.string_at(err.contents.payload_ptr, err.contents.payload_len) if err.contents.payload_ptr else b"" - _lib.weaveffi_error_free(err) - _state["err"] = _task_error_from(_code, _msg, _payload) - else: - try: - _state["val"] = _unpack_TaskResult(_take_buffer(ctypes.cast(result_ptr, ctypes.c_void_p).value, result_len)) - except Exception as _exc: - _state["err"] = _exc - - def _resolve(): - _async_pending.pop(_token, None) - # A cancelled future must not be resolved. - if _fut.cancelled(): - return - if _state["err"] is not None: - _fut.set_exception(_state["err"]) - else: - _fut.set_result(_state["val"]) - - _loop.call_soon_threadsafe(_resolve) - - _cb_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct), ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t) - _cb = _cb_type(_cb_impl) - _token = _async_register(_cb) # pinned until completion - _fn.argtypes = [ctypes.c_char_p, _cb_type, ctypes.c_void_p] - _fn.restype = None - _fn(_string_to_bytes(name), _cb, None) - return await _fut - - -async def run_batch(names: List[str]) -> List["TaskResult"]: - _fn = _lib.weaveffi_tasks_run_batch_async - _loop = asyncio.get_running_loop() - _fut = _loop.create_future() - - def _cb_impl(context, err, result_ptr, result_len): - # Fires exactly once, on a producer thread: take ownership of - # the result here, then hop back to the event loop. - _state = {"err": None, "val": None} - if err and err.contents.code != 0: - _code = err.contents.code - _msg = err.contents.message.decode("utf-8") if err.contents.message else "" - _lib.weaveffi_error_free(err) - _state["err"] = WeaveFFIError(_code, _msg) - else: - try: - _state["val"] = _decode_buffer(_take_buffer(ctypes.cast(result_ptr, ctypes.c_void_p).value, result_len), lambda _r: [_read_TaskResult(_r) for _i0 in range(_r.read_len())]) - except Exception as _exc: - _state["err"] = _exc - - def _resolve(): - _async_pending.pop(_token, None) - # A cancelled future must not be resolved. - if _fut.cancelled(): - return - if _state["err"] is not None: - _fut.set_exception(_state["err"]) - else: - _fut.set_result(_state["val"]) - - _loop.call_soon_threadsafe(_resolve) - - _cb_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct), ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t) - _cb = _cb_type(_cb_impl) - _token = _async_register(_cb) # pinned until completion - _fn.argtypes = [ctypes.c_char_p, ctypes.c_size_t, _cb_type, ctypes.c_void_p] - _fn.restype = None - _names_w = _BufferWriter() - _names_w.write_len(len(names)) - for _e0 in names: - _names_w.write_string(_e0) - _names_buf = _names_w.finish() - _fn(_names_buf, len(_names_buf), _cb, None) - return await _fut - - -def cancel_task(id: int) -> bool: - _fn = _lib.weaveffi_tasks_cancel_task - _fn.argtypes = [ctypes.c_int64, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(id, ctypes.byref(_err)) - _check_error(_err) - return bool(_result) - - -async def run_n_tasks(n: int) -> int: - _fn = _lib.weaveffi_tasks_run_n_tasks_async - _loop = asyncio.get_running_loop() - _fut = _loop.create_future() - - def _cb_impl(context, err, result): - # Fires exactly once, on a producer thread: take ownership of - # the result here, then hop back to the event loop. - _state = {"err": None, "val": None} - if err and err.contents.code != 0: - _code = err.contents.code - _msg = err.contents.message.decode("utf-8") if err.contents.message else "" - _lib.weaveffi_error_free(err) - _state["err"] = WeaveFFIError(_code, _msg) - else: - try: - _state["val"] = result - except Exception as _exc: - _state["err"] = _exc - - def _resolve(): - _async_pending.pop(_token, None) - # A cancelled future must not be resolved. - if _fut.cancelled(): - return - if _state["err"] is not None: - _fut.set_exception(_state["err"]) - else: - _fut.set_result(_state["val"]) - - _loop.call_soon_threadsafe(_resolve) - - _cb_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct), ctypes.c_int32) - _cb = _cb_type(_cb_impl) - _token = _async_register(_cb) # pinned until completion - _fn.argtypes = [ctypes.c_int32, _cb_type, ctypes.c_void_p] - _fn.restype = None - _fn(n, _cb, None) - return await _fut - - -def active_callbacks() -> int: - _fn = _lib.weaveffi_tasks_active_callbacks - _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int64 - _err = _WeaveFFIErrorStruct() - _result = _fn(ctypes.byref(_err)) - _check_error(_err) - return _result - -# END weaveffi.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_async_demo__weaveffi_weaveffi_pyi.snap b/crates/weaveffi-cli/tests/snapshots/python_async_demo__weaveffi_weaveffi_pyi.snap deleted file mode 100644 index 38aec8bc..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_async_demo__weaveffi_weaveffi_pyi.snap +++ /dev/null @@ -1,40 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional, Type - -class WeaveFFIError(Exception): - code: int - message: str - def __init__(self, code: int, message: str) -> None: ... - -class TaskError(WeaveFFIError): - InvalidName: Type["InvalidName"] - def __init__(self, code: int, message: str) -> None: ... - -class InvalidName(TaskError): - CODE: int - def __init__(self, message: str = ...) -> None: ... - -class TaskResult: - id: int - value: str - success: bool - def __init__(self, id: int, value: str, success: bool) -> None: ... - -async def run_task(name: str) -> "TaskResult": ... - -async def run_batch(names: List[str]) -> List["TaskResult"]: ... - -def cancel_task(id: int) -> bool: ... - -async def run_n_tasks(n: int) -> int: ... - -def active_callbacks() -> int: ... - -# END weaveffi.pyi diff --git a/crates/weaveffi-cli/tests/snapshots/python_calculator__README_md.snap b/crates/weaveffi-cli/tests/snapshots/python_calculator__README_md.snap deleted file mode 100644 index e3b8a7b9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_calculator__README_md.snap +++ /dev/null @@ -1,35 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# weaveffi (Python) - -Auto-generated Python bindings using ctypes. - -## Prerequisites - -- Python >= 3.8 -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path. - -## Install - -```bash -pip install . -``` - -## Development install - -```bash -pip install -e . -``` - -## Usage - -```python -from weaveffi import * -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/python_calculator__pyproject_toml.snap b/crates/weaveffi-cli/tests/snapshots/python_calculator__pyproject_toml.snap deleted file mode 100644 index c07695fa..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_calculator__pyproject_toml.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "weaveffi" -version = "0.1.0" -description = "weaveffi bindings generated by WeaveFFI" -requires-python = ">=3.8" - -[tool.setuptools] -packages = ["weaveffi"] - -# END pyproject.toml diff --git a/crates/weaveffi-cli/tests/snapshots/python_calculator__setup_py.snap b/crates/weaveffi-cli/tests/snapshots/python_calculator__setup_py.snap deleted file mode 100644 index 4c5da6f5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_calculator__setup_py.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -from setuptools import setup - -setup( - name="weaveffi", - version="0.1.0", - packages=["weaveffi"], -) - -# END setup.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_calculator__weaveffi___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_calculator__weaveffi___init___py.snap deleted file mode 100644 index e4ae4ecc..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_calculator__weaveffi___init___py.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -from .weaveffi import * # noqa: F401,F403 - -# END __init__.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_calculator__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_calculator__weaveffi_weaveffi_py.snap deleted file mode 100644 index bfde59ea..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_calculator__weaveffi_weaveffi_py.snap +++ /dev/null @@ -1,362 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -"""WeaveFFI Python ctypes bindings (auto-generated)""" -import contextlib -import ctypes -import os -import platform -import struct -from dataclasses import dataclass -from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional - - -class WeaveFFIError(Exception): - def __init__(self, code: int, message: str) -> None: - self.code = code - self.message = message - super().__init__(f"({code}) {message}") - - -class _WeaveFFIErrorStruct(ctypes.Structure): - _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 in WEAVEFFI_LIBRARY wins, so callers can point at a - # specific build artifact regardless of its file name or location. - override = os.environ.get("WEAVEFFI_LIBRARY") - if override: - return ctypes.CDLL(override) - system = platform.system() - if system == "Darwin": - name = "libweaveffi.dylib" - elif system == "Windows": - name = "weaveffi.dll" - else: - name = "libweaveffi.so" - return ctypes.CDLL(name) - - -_lib = _load_library() -_lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] -_lib.weaveffi_error_clear.restype = None -# Async completion callbacks receive a heap-boxed error the consumer owns; -# weaveffi_error_free releases the message, the payload, and the box. -_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] -_lib.weaveffi_error_free.restype = None -# The free helpers take raw addresses (`c_void_p`) so wrappers can release -# owned producer allocations they hold as plain integers or typed pointers. -_lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] -_lib.weaveffi_free_string.restype = None -_lib.weaveffi_free_bytes.argtypes = [ctypes.c_void_p, ctypes.c_size_t] -_lib.weaveffi_free_bytes.restype = None - - -def _check_error(err: _WeaveFFIErrorStruct) -> None: - if err.code != 0: - code = err.code - message = err.message.decode("utf-8") if err.message else "" - _lib.weaveffi_error_clear(ctypes.byref(err)) - raise WeaveFFIError(code, message) - - -class _PointerGuard(contextlib.AbstractContextManager): - def __init__(self, ptr, free_fn) -> None: - self.ptr = ptr - self._free_fn = free_fn - - def __exit__(self, *exc) -> bool: - if self.ptr is not None: - self._free_fn(self.ptr) - self.ptr = None - return False - - -def _string_to_bytes(s: Optional[str]) -> Optional[bytes]: - if s is None: - return None - return s.encode("utf-8") - - -def _bytes_to_string(ptr) -> Optional[str]: - if ptr is None: - return None - return ptr.decode("utf-8") - - -def _take_string(ptr) -> Optional[str]: - """Copy an owned C string (a raw address) and free the producer buffer.""" - if not ptr: - return None - _s = ctypes.string_at(ptr).decode("utf-8") - _lib.weaveffi_free_string(ptr) - return _s - - -class _BufferWriter: - """Encodes values into the WeaveFFI value-buffer wire format: - little-endian, packed, no alignment.""" - - def __init__(self) -> None: - self._buf = bytearray() - - def finish(self) -> bytes: - return bytes(self._buf) - - def write_bool(self, v) -> None: - self._buf.append(1 if v else 0) - - def write_i8(self, v: int) -> None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf.append(1 if present else 0) - - def write_string(self, v: str) -> None: - _b = v.encode("utf-8") - self.write_len(len(_b)) - self._buf += _b - - def write_bytes(self, v: bytes) -> None: - self.write_len(len(v)) - self._buf += bytes(v) - - -class _BufferReader: - """Decodes values from the WeaveFFI value-buffer wire format, rejecting - truncated buffers, invalid flag bytes, invalid UTF-8, and oversized - length prefixes.""" - - def __init__(self, data: bytes) -> None: - self._data = memoryview(data) - self._pos = 0 - - def _take(self, n: int, what: str) -> memoryview: - if len(self._data) - self._pos < n: - raise WeaveFFIError(-1, f"malformed value buffer: truncated {what}") - _view = self._data[self._pos:self._pos + n] - self._pos += n - return _view - - def read_bool(self) -> bool: - _b = self._take(1, "bool")[0] - if _b > 1: - raise WeaveFFIError(-1, "malformed value buffer: invalid bool byte") - return _b == 1 - - def read_i8(self) -> int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" float: - return struct.unpack(" float: - return struct.unpack(" int: - _n = struct.unpack(" len(self._data) - self._pos: - raise WeaveFFIError( - -1, "malformed value buffer: length prefix exceeds remaining bytes" - ) - return _n - - def read_option_flag(self) -> bool: - _b = self._take(1, "option flag")[0] - if _b > 1: - raise WeaveFFIError(-1, "malformed value buffer: invalid option flag") - return _b == 1 - - def read_string(self) -> str: - _n = self.read_len() - try: - return str(self._take(_n, "string data"), "utf-8") - except UnicodeDecodeError as _e: - raise WeaveFFIError( - -1, "malformed value buffer: string is not valid UTF-8" - ) from _e - - def read_bytes(self) -> bytes: - _n = self.read_len() - return bytes(self._take(_n, "bytes data")) - - def expect_end(self) -> None: - if self._pos != len(self._data): - raise WeaveFFIError(-1, "malformed value buffer: trailing bytes") - - -def _decode_buffer(data: bytes, read_fn): - """Decode exactly one value from `data` using `read_fn(reader)`.""" - _r = _BufferReader(data) - _v = read_fn(_r) - _r.expect_end() - return _v - - -def _take_buffer(ptr, length) -> bytes: - """Copy an owned value buffer (a raw address) and release it with - weaveffi_free_bytes.""" - if not ptr: - return b"" - _data = ctypes.string_at(ptr, length) if length else b"" - _lib.weaveffi_free_bytes(ptr, ctypes.c_size_t(length)) - return _data - - -# === Module: calculator === - -class CalcError(WeaveFFIError): - """Base exception for the `calculator` module's error domain.""" - - -class DivisionByZero(CalcError): - """division by zero""" - - CODE = 1 - - def __init__(self, message: str = "division by zero") -> None: - super().__init__(1, message) - - -CalcError.DivisionByZero = DivisionByZero - - -_CALC_ERROR_CODES: Dict[int, type] = { - 1: DivisionByZero, -} - - -def _calc_error_from(code: int, message: str, payload: bytes = b"") -> WeaveFFIError: - """Build the CalcError subclass matching `code`, or a generic - WeaveFFIError for codes outside the domain (panics, marshalling).""" - _cls = _CALC_ERROR_CODES.get(code) - if _cls is None: - return WeaveFFIError(code, message) - _exc = _cls(message) if message else _cls() - return _exc - - -def _check_calc_error(err: _WeaveFFIErrorStruct) -> None: - if err.code != 0: - code = err.code - message = err.message.decode("utf-8") if err.message else "" - payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" - _lib.weaveffi_error_clear(ctypes.byref(err)) - raise _calc_error_from(code, message, payload) - - -def add(a: int, b: int) -> int: - """Add two integers - """ - _fn = _lib.weaveffi_calculator_add - _fn.argtypes = [ctypes.c_int32, ctypes.c_int32, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(a, b, ctypes.byref(_err)) - _check_error(_err) - return _result - - -def mul(a: int, b: int) -> int: - """Multiply two integers - """ - _fn = _lib.weaveffi_calculator_mul - _fn.argtypes = [ctypes.c_int32, ctypes.c_int32, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(a, b, ctypes.byref(_err)) - _check_error(_err) - return _result - - -def div(a: int, b: int) -> int: - """Divide two integers, failing on a zero divisor - - Raises - ------ - CalcError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_calculator_div - _fn.argtypes = [ctypes.c_int32, ctypes.c_int32, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(a, b, ctypes.byref(_err)) - _check_calc_error(_err) - return _result - - -def echo(s: str) -> str: - """Echo a string back - """ - _fn = _lib.weaveffi_calculator_echo - _fn.argtypes = [ctypes.c_char_p, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _result = _fn(_string_to_bytes(s), ctypes.byref(_err)) - _check_error(_err) - return _take_string(_result) or "" - -# END weaveffi.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_calculator__weaveffi_weaveffi_pyi.snap b/crates/weaveffi-cli/tests/snapshots/python_calculator__weaveffi_weaveffi_pyi.snap deleted file mode 100644 index 6c4bdf3c..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_calculator__weaveffi_weaveffi_pyi.snap +++ /dev/null @@ -1,36 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional, Type - -class WeaveFFIError(Exception): - code: int - message: str - def __init__(self, code: int, message: str) -> None: ... - -class CalcError(WeaveFFIError): - DivisionByZero: Type["DivisionByZero"] - def __init__(self, code: int, message: str) -> None: ... - -class DivisionByZero(CalcError): - CODE: int - def __init__(self, message: str = ...) -> None: ... - -# Add two integers -def add(a: int, b: int) -> int: ... - -# Multiply two integers -def mul(a: int, b: int) -> int: ... - -# Divide two integers, failing on a zero divisor -def div(a: int, b: int) -> int: ... - -# Echo a string back -def echo(s: str) -> str: ... - -# END weaveffi.pyi diff --git a/crates/weaveffi-cli/tests/snapshots/python_contacts__README_md.snap b/crates/weaveffi-cli/tests/snapshots/python_contacts__README_md.snap deleted file mode 100644 index e3b8a7b9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_contacts__README_md.snap +++ /dev/null @@ -1,35 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# weaveffi (Python) - -Auto-generated Python bindings using ctypes. - -## Prerequisites - -- Python >= 3.8 -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path. - -## Install - -```bash -pip install . -``` - -## Development install - -```bash -pip install -e . -``` - -## Usage - -```python -from weaveffi import * -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/python_contacts__pyproject_toml.snap b/crates/weaveffi-cli/tests/snapshots/python_contacts__pyproject_toml.snap deleted file mode 100644 index c07695fa..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_contacts__pyproject_toml.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "weaveffi" -version = "0.1.0" -description = "weaveffi bindings generated by WeaveFFI" -requires-python = ">=3.8" - -[tool.setuptools] -packages = ["weaveffi"] - -# END pyproject.toml diff --git a/crates/weaveffi-cli/tests/snapshots/python_contacts__setup_py.snap b/crates/weaveffi-cli/tests/snapshots/python_contacts__setup_py.snap deleted file mode 100644 index 4c5da6f5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_contacts__setup_py.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -from setuptools import setup - -setup( - name="weaveffi", - version="0.1.0", - packages=["weaveffi"], -) - -# END setup.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_contacts__weaveffi___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_contacts__weaveffi___init___py.snap deleted file mode 100644 index e4ae4ecc..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_contacts__weaveffi___init___py.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -from .weaveffi import * # noqa: F401,F403 - -# END __init__.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_contacts__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_contacts__weaveffi_weaveffi_py.snap deleted file mode 100644 index b6191e71..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_contacts__weaveffi_weaveffi_py.snap +++ /dev/null @@ -1,472 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -"""WeaveFFI Python ctypes bindings (auto-generated)""" -import contextlib -import ctypes -import os -import platform -import struct -from dataclasses import dataclass -from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional - - -class WeaveFFIError(Exception): - def __init__(self, code: int, message: str) -> None: - self.code = code - self.message = message - super().__init__(f"({code}) {message}") - - -class _WeaveFFIErrorStruct(ctypes.Structure): - _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 in WEAVEFFI_LIBRARY wins, so callers can point at a - # specific build artifact regardless of its file name or location. - override = os.environ.get("WEAVEFFI_LIBRARY") - if override: - return ctypes.CDLL(override) - system = platform.system() - if system == "Darwin": - name = "libweaveffi.dylib" - elif system == "Windows": - name = "weaveffi.dll" - else: - name = "libweaveffi.so" - return ctypes.CDLL(name) - - -_lib = _load_library() -_lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] -_lib.weaveffi_error_clear.restype = None -# Async completion callbacks receive a heap-boxed error the consumer owns; -# weaveffi_error_free releases the message, the payload, and the box. -_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] -_lib.weaveffi_error_free.restype = None -# The free helpers take raw addresses (`c_void_p`) so wrappers can release -# owned producer allocations they hold as plain integers or typed pointers. -_lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] -_lib.weaveffi_free_string.restype = None -_lib.weaveffi_free_bytes.argtypes = [ctypes.c_void_p, ctypes.c_size_t] -_lib.weaveffi_free_bytes.restype = None - - -def _check_error(err: _WeaveFFIErrorStruct) -> None: - if err.code != 0: - code = err.code - message = err.message.decode("utf-8") if err.message else "" - _lib.weaveffi_error_clear(ctypes.byref(err)) - raise WeaveFFIError(code, message) - - -class _PointerGuard(contextlib.AbstractContextManager): - def __init__(self, ptr, free_fn) -> None: - self.ptr = ptr - self._free_fn = free_fn - - def __exit__(self, *exc) -> bool: - if self.ptr is not None: - self._free_fn(self.ptr) - self.ptr = None - return False - - -def _string_to_bytes(s: Optional[str]) -> Optional[bytes]: - if s is None: - return None - return s.encode("utf-8") - - -def _bytes_to_string(ptr) -> Optional[str]: - if ptr is None: - return None - return ptr.decode("utf-8") - - -def _take_string(ptr) -> Optional[str]: - """Copy an owned C string (a raw address) and free the producer buffer.""" - if not ptr: - return None - _s = ctypes.string_at(ptr).decode("utf-8") - _lib.weaveffi_free_string(ptr) - return _s - - -class _BufferWriter: - """Encodes values into the WeaveFFI value-buffer wire format: - little-endian, packed, no alignment.""" - - def __init__(self) -> None: - self._buf = bytearray() - - def finish(self) -> bytes: - return bytes(self._buf) - - def write_bool(self, v) -> None: - self._buf.append(1 if v else 0) - - def write_i8(self, v: int) -> None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf.append(1 if present else 0) - - def write_string(self, v: str) -> None: - _b = v.encode("utf-8") - self.write_len(len(_b)) - self._buf += _b - - def write_bytes(self, v: bytes) -> None: - self.write_len(len(v)) - self._buf += bytes(v) - - -class _BufferReader: - """Decodes values from the WeaveFFI value-buffer wire format, rejecting - truncated buffers, invalid flag bytes, invalid UTF-8, and oversized - length prefixes.""" - - def __init__(self, data: bytes) -> None: - self._data = memoryview(data) - self._pos = 0 - - def _take(self, n: int, what: str) -> memoryview: - if len(self._data) - self._pos < n: - raise WeaveFFIError(-1, f"malformed value buffer: truncated {what}") - _view = self._data[self._pos:self._pos + n] - self._pos += n - return _view - - def read_bool(self) -> bool: - _b = self._take(1, "bool")[0] - if _b > 1: - raise WeaveFFIError(-1, "malformed value buffer: invalid bool byte") - return _b == 1 - - def read_i8(self) -> int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" float: - return struct.unpack(" float: - return struct.unpack(" int: - _n = struct.unpack(" len(self._data) - self._pos: - raise WeaveFFIError( - -1, "malformed value buffer: length prefix exceeds remaining bytes" - ) - return _n - - def read_option_flag(self) -> bool: - _b = self._take(1, "option flag")[0] - if _b > 1: - raise WeaveFFIError(-1, "malformed value buffer: invalid option flag") - return _b == 1 - - def read_string(self) -> str: - _n = self.read_len() - try: - return str(self._take(_n, "string data"), "utf-8") - except UnicodeDecodeError as _e: - raise WeaveFFIError( - -1, "malformed value buffer: string is not valid UTF-8" - ) from _e - - def read_bytes(self) -> bytes: - _n = self.read_len() - return bytes(self._take(_n, "bytes data")) - - def expect_end(self) -> None: - if self._pos != len(self._data): - raise WeaveFFIError(-1, "malformed value buffer: trailing bytes") - - -def _decode_buffer(data: bytes, read_fn): - """Decode exactly one value from `data` using `read_fn(reader)`.""" - _r = _BufferReader(data) - _v = read_fn(_r) - _r.expect_end() - return _v - - -def _take_buffer(ptr, length) -> bytes: - """Copy an owned value buffer (a raw address) and release it with - weaveffi_free_bytes.""" - if not ptr: - return b"" - _data = ctypes.string_at(ptr, length) if length else b"" - _lib.weaveffi_free_bytes(ptr, ctypes.c_size_t(length)) - return _data - - -# === Module: contacts === - -class ContactsError(WeaveFFIError): - """Base exception for the `contacts` module's error domain.""" - - -class InvalidName(ContactsError): - """name must not be empty""" - - CODE = 1 - - def __init__(self, message: str = "name must not be empty") -> None: - super().__init__(1, message) - - -class NotFound(ContactsError): - """contact not found""" - - CODE = 2 - - def __init__(self, message: str = "contact not found") -> None: - super().__init__(2, message) - - -ContactsError.InvalidName = InvalidName -ContactsError.NotFound = NotFound - - -_CONTACTS_ERROR_CODES: Dict[int, type] = { - 1: InvalidName, - 2: NotFound, -} - - -def _contacts_error_from(code: int, message: str, payload: bytes = b"") -> WeaveFFIError: - """Build the ContactsError subclass matching `code`, or a generic - WeaveFFIError for codes outside the domain (panics, marshalling).""" - _cls = _CONTACTS_ERROR_CODES.get(code) - if _cls is None: - return WeaveFFIError(code, message) - _exc = _cls(message) if message else _cls() - return _exc - - -def _check_contacts_error(err: _WeaveFFIErrorStruct) -> None: - if err.code != 0: - code = err.code - message = err.message.decode("utf-8") if err.message else "" - payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" - _lib.weaveffi_error_clear(ctypes.byref(err)) - raise _contacts_error_from(code, message, payload) - - -class ContactType(IntEnum): - Personal = 0 - Work = 1 - Other = 2 - - -@dataclass -class Contact: - id: int - first_name: str - last_name: str - email: Optional[str] - contact_type: "ContactType" - - -def _write_Contact(_w: _BufferWriter, value: "Contact") -> None: - _w.write_i64(value.id) - _w.write_string(value.first_name) - _w.write_string(value.last_name) - if value.email is None: - _w.write_option_flag(False) - else: - _w.write_option_flag(True) - _w.write_string(value.email) - _w.write_i32(value.contact_type) - - -def _read_Contact(_r: _BufferReader) -> "Contact": - return Contact( - id=_r.read_i64(), - first_name=_r.read_string(), - last_name=_r.read_string(), - email=(_r.read_string() if _r.read_option_flag() else None), - contact_type=ContactType(_r.read_i32()), - ) - - -def _pack_Contact(value: "Contact") -> bytes: - _w = _BufferWriter() - _write_Contact(_w, value) - return _w.finish() - - -def _unpack_Contact(data: bytes) -> "Contact": - return _decode_buffer(data, _read_Contact) - - -class ContactBook: - """An in-memory address book owning its contacts""" - - @classmethod - def _from_ptr(cls, ptr) -> "ContactBook": - _obj = cls.__new__(cls) - _obj._ptr = ptr - return _obj - - def __init__(self) -> None: - self._ptr = None - _fn = _lib.weaveffi_contacts_ContactBook_new - _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _result = _fn(ctypes.byref(_err)) - _check_error(_err) - if _result is None: - raise WeaveFFIError(-1, "null pointer") - self._ptr = _result - - - def __del__(self) -> None: - if self._ptr is not None: - _lib.weaveffi_contacts_ContactBook_destroy.argtypes = [ctypes.c_void_p] - _lib.weaveffi_contacts_ContactBook_destroy.restype = None - _lib.weaveffi_contacts_ContactBook_destroy(self._ptr) - self._ptr = None - - def add(self, first_name: str, last_name: str, email: Optional[str], contact_type: "ContactType") -> "Contact": - """ - Raises - ------ - ContactsError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_contacts_ContactBook_add - _fn.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_int32, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _email_w = _BufferWriter() - if email is None: - _email_w.write_option_flag(False) - else: - _email_w.write_option_flag(True) - _email_w.write_string(email) - _email_buf = _email_w.finish() - _err = _WeaveFFIErrorStruct() - _out_len = ctypes.c_size_t(0) - _result = _fn(self._ptr, _string_to_bytes(first_name), _string_to_bytes(last_name), _email_buf, len(_email_buf), contact_type.value, ctypes.byref(_out_len), ctypes.byref(_err)) - _check_contacts_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_Contact(_data) - - - def get(self, id: int) -> "Contact": - """ - Raises - ------ - ContactsError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_contacts_ContactBook_get - _fn.argtypes = [ctypes.c_void_p, ctypes.c_int64, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _out_len = ctypes.c_size_t(0) - _result = _fn(self._ptr, id, ctypes.byref(_out_len), ctypes.byref(_err)) - _check_contacts_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_Contact(_data) - - - def list(self) -> List["Contact"]: - _fn = _lib.weaveffi_contacts_ContactBook_list - _fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _out_len = ctypes.c_size_t(0) - _result = _fn(self._ptr, ctypes.byref(_out_len), ctypes.byref(_err)) - _check_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _decode_buffer(_data, lambda _r: [_read_Contact(_r) for _i0 in range(_r.read_len())]) - - - def remove(self, id: int) -> bool: - _fn = _lib.weaveffi_contacts_ContactBook_remove - _fn.argtypes = [ctypes.c_void_p, ctypes.c_int64, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(self._ptr, id, ctypes.byref(_err)) - _check_error(_err) - return bool(_result) - - - def count(self) -> int: - _fn = _lib.weaveffi_contacts_ContactBook_count - _fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(self._ptr, ctypes.byref(_err)) - _check_error(_err) - return _result - - -# END weaveffi.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_contacts__weaveffi_weaveffi_pyi.snap b/crates/weaveffi-cli/tests/snapshots/python_contacts__weaveffi_weaveffi_pyi.snap deleted file mode 100644 index a8dc06f6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_contacts__weaveffi_weaveffi_pyi.snap +++ /dev/null @@ -1,51 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional, Type - -class WeaveFFIError(Exception): - code: int - message: str - def __init__(self, code: int, message: str) -> None: ... - -class ContactsError(WeaveFFIError): - InvalidName: Type["InvalidName"] - NotFound: Type["NotFound"] - def __init__(self, code: int, message: str) -> None: ... - -class InvalidName(ContactsError): - CODE: int - def __init__(self, message: str = ...) -> None: ... - -class NotFound(ContactsError): - CODE: int - def __init__(self, message: str = ...) -> None: ... - -class ContactType(IntEnum): - Personal: int - Work: int - Other: int - -class Contact: - id: int - first_name: str - last_name: str - email: Optional[str] - contact_type: "ContactType" - def __init__(self, id: int, first_name: str, last_name: str, email: Optional[str], contact_type: "ContactType") -> None: ... - -# An in-memory address book owning its contacts -class ContactBook: - def __init__(self) -> None: ... - def add(self, first_name: str, last_name: str, email: Optional[str], contact_type: "ContactType") -> "Contact": ... - def get(self, id: int) -> "Contact": ... - def list(self) -> List["Contact"]: ... - def remove(self, id: int) -> bool: ... - def count(self) -> int: ... - -# END weaveffi.pyi diff --git a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__weaveffi_weaveffi_py.snap index 9bca75c0..0a6b5094 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__weaveffi_weaveffi_py.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_docs_everywhere__weaveffi_weaveffi_py.snap @@ -49,6 +49,32 @@ def _load_library() -> ctypes.CDLL: _lib = _load_library() + +# The ABI revision these bindings were generated against. Checked before any +# other symbol is bound so a mismatched producer fails at import time instead +# of misreading the error struct or a value buffer later. +_ABI_VERSION = 1 + + +def _check_abi_version(lib: ctypes.CDLL) -> None: + try: + fn = lib.weaveffi_abi_version + except AttributeError: + raise ImportError( + "the loaded WeaveFFI library predates ABI versioning " + f"(these bindings expect ABI revision {_ABI_VERSION})" + ) from None + fn.argtypes = [] + fn.restype = ctypes.c_uint32 + found = fn() + if found != _ABI_VERSION: + raise ImportError( + f"WeaveFFI ABI mismatch: these bindings expect revision {_ABI_VERSION} " + f"but the loaded library reports revision {found}" + ) + + +_check_abi_version(_lib) _lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] _lib.weaveffi_error_clear.restype = None # Async completion callbacks receive a heap-boxed error the consumer owns; diff --git a/crates/weaveffi-cli/tests/snapshots/python_async_demo__README_md.snap b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__README_md.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/python_async_demo__README_md.snap rename to crates/weaveffi-cli/tests/snapshots/python_edge_cases__README_md.snap diff --git a/crates/weaveffi-cli/tests/snapshots/python_async_demo__pyproject_toml.snap b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__pyproject_toml.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/python_async_demo__pyproject_toml.snap rename to crates/weaveffi-cli/tests/snapshots/python_edge_cases__pyproject_toml.snap diff --git a/crates/weaveffi-cli/tests/snapshots/python_async_demo__setup_py.snap b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__setup_py.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/python_async_demo__setup_py.snap rename to crates/weaveffi-cli/tests/snapshots/python_edge_cases__setup_py.snap diff --git a/crates/weaveffi-cli/tests/snapshots/python_async_demo__weaveffi___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__weaveffi___init___py.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/python_async_demo__weaveffi___init___py.snap rename to crates/weaveffi-cli/tests/snapshots/python_edge_cases__weaveffi___init___py.snap diff --git a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__weaveffi_weaveffi_py.snap new file mode 100644 index 00000000..fb85f35e --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__weaveffi_weaveffi_py.snap @@ -0,0 +1,1682 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from weaveffi.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate weaveffi.yml -o + +"""WeaveFFI Python ctypes bindings (auto-generated)""" +import contextlib +import ctypes +import os +import platform +import struct +from dataclasses import dataclass +from enum import IntEnum +from typing import Callable, Dict, Iterator, List, Optional + + +class WeaveFFIError(Exception): + def __init__(self, code: int, message: str) -> None: + self.code = code + self.message = message + super().__init__(f"({code}) {message}") + + +class _WeaveFFIErrorStruct(ctypes.Structure): + _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 in WEAVEFFI_LIBRARY wins, so callers can point at a + # specific build artifact regardless of its file name or location. + override = os.environ.get("WEAVEFFI_LIBRARY") + if override: + return ctypes.CDLL(override) + system = platform.system() + if system == "Darwin": + name = "libweaveffi.dylib" + elif system == "Windows": + name = "weaveffi.dll" + else: + name = "libweaveffi.so" + return ctypes.CDLL(name) + + +_lib = _load_library() + +# The ABI revision these bindings were generated against. Checked before any +# other symbol is bound so a mismatched producer fails at import time instead +# of misreading the error struct or a value buffer later. +_ABI_VERSION = 1 + + +def _check_abi_version(lib: ctypes.CDLL) -> None: + try: + fn = lib.weaveffi_abi_version + except AttributeError: + raise ImportError( + "the loaded WeaveFFI library predates ABI versioning " + f"(these bindings expect ABI revision {_ABI_VERSION})" + ) from None + fn.argtypes = [] + fn.restype = ctypes.c_uint32 + found = fn() + if found != _ABI_VERSION: + raise ImportError( + f"WeaveFFI ABI mismatch: these bindings expect revision {_ABI_VERSION} " + f"but the loaded library reports revision {found}" + ) + + +_check_abi_version(_lib) +_lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] +_lib.weaveffi_error_clear.restype = None +# Async completion callbacks receive a heap-boxed error the consumer owns; +# weaveffi_error_free releases the message, the payload, and the box. +_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] +_lib.weaveffi_error_free.restype = None +# The free helpers take raw addresses (`c_void_p`) so wrappers can release +# owned producer allocations they hold as plain integers or typed pointers. +_lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] +_lib.weaveffi_free_string.restype = None +_lib.weaveffi_free_bytes.argtypes = [ctypes.c_void_p, ctypes.c_size_t] +_lib.weaveffi_free_bytes.restype = None + + +def _check_error(err: _WeaveFFIErrorStruct) -> None: + if err.code != 0: + code = err.code + message = err.message.decode("utf-8") if err.message else "" + _lib.weaveffi_error_clear(ctypes.byref(err)) + raise WeaveFFIError(code, message) + + +class _PointerGuard(contextlib.AbstractContextManager): + def __init__(self, ptr, free_fn) -> None: + self.ptr = ptr + self._free_fn = free_fn + + def __exit__(self, *exc) -> bool: + if self.ptr is not None: + self._free_fn(self.ptr) + self.ptr = None + return False + + +def _string_to_bytes(s: Optional[str]) -> Optional[bytes]: + if s is None: + return None + return s.encode("utf-8") + + +def _bytes_to_string(ptr) -> Optional[str]: + if ptr is None: + return None + return ptr.decode("utf-8") + + +def _take_string(ptr) -> Optional[str]: + """Copy an owned C string (a raw address) and free the producer buffer.""" + if not ptr: + return None + _s = ctypes.string_at(ptr).decode("utf-8") + _lib.weaveffi_free_string(ptr) + return _s + + +class _BufferWriter: + """Encodes values into the WeaveFFI value-buffer wire format: + little-endian, packed, no alignment.""" + + def __init__(self) -> None: + self._buf = bytearray() + + def finish(self) -> bytes: + return bytes(self._buf) + + def write_bool(self, v) -> None: + self._buf.append(1 if v else 0) + + def write_i8(self, v: int) -> None: + self._buf += struct.pack(" None: + self._buf += struct.pack(" None: + self._buf += struct.pack(" None: + self._buf += struct.pack(" None: + self._buf += struct.pack(" None: + self._buf += struct.pack(" None: + self._buf += struct.pack(" None: + self._buf += struct.pack(" None: + self._buf += struct.pack(" None: + self._buf += struct.pack(" None: + self._buf += struct.pack(" None: + self._buf.append(1 if present else 0) + + def write_string(self, v: str) -> None: + _b = v.encode("utf-8") + self.write_len(len(_b)) + self._buf += _b + + def write_bytes(self, v: bytes) -> None: + self.write_len(len(v)) + self._buf += bytes(v) + + +class _BufferReader: + """Decodes values from the WeaveFFI value-buffer wire format, rejecting + truncated buffers, invalid flag bytes, invalid UTF-8, and oversized + length prefixes.""" + + def __init__(self, data: bytes) -> None: + self._data = memoryview(data) + self._pos = 0 + + def _take(self, n: int, what: str) -> memoryview: + if len(self._data) - self._pos < n: + raise WeaveFFIError(-1, f"malformed value buffer: truncated {what}") + _view = self._data[self._pos:self._pos + n] + self._pos += n + return _view + + def read_bool(self) -> bool: + _b = self._take(1, "bool")[0] + if _b > 1: + raise WeaveFFIError(-1, "malformed value buffer: invalid bool byte") + return _b == 1 + + def read_i8(self) -> int: + return struct.unpack(" int: + return struct.unpack(" int: + return struct.unpack(" int: + return struct.unpack(" int: + return struct.unpack(" int: + return struct.unpack(" int: + return struct.unpack(" int: + return struct.unpack(" float: + return struct.unpack(" float: + return struct.unpack(" int: + _n = struct.unpack(" len(self._data) - self._pos: + raise WeaveFFIError( + -1, "malformed value buffer: length prefix exceeds remaining bytes" + ) + return _n + + def read_option_flag(self) -> bool: + _b = self._take(1, "option flag")[0] + if _b > 1: + raise WeaveFFIError(-1, "malformed value buffer: invalid option flag") + return _b == 1 + + def read_string(self) -> str: + _n = self.read_len() + try: + return str(self._take(_n, "string data"), "utf-8") + except UnicodeDecodeError as _e: + raise WeaveFFIError( + -1, "malformed value buffer: string is not valid UTF-8" + ) from _e + + def read_bytes(self) -> bytes: + _n = self.read_len() + return bytes(self._take(_n, "bytes data")) + + def expect_end(self) -> None: + if self._pos != len(self._data): + raise WeaveFFIError(-1, "malformed value buffer: trailing bytes") + + +def _decode_buffer(data: bytes, read_fn): + """Decode exactly one value from `data` using `read_fn(reader)`.""" + _r = _BufferReader(data) + _v = read_fn(_r) + _r.expect_end() + return _v + + +def _take_buffer(ptr, length) -> bytes: + """Copy an owned value buffer (a raw address) and release it with + weaveffi_free_bytes.""" + if not ptr: + return b"" + _data = ctypes.string_at(ptr, length) if length else b"" + _lib.weaveffi_free_bytes(ptr, ctypes.c_size_t(length)) + return _data + +import asyncio +import threading + + +# Pending async completion trampolines, keyed by an integer token. +# Holding the ctypes function objects here keeps them alive until the +# producer fires the completion callback, even when the awaiting +# coroutine has been cancelled; each entry is removed on completion. +_async_pending: Dict[int, object] = {} +_async_lock = threading.Lock() +_async_next_token = 0 + + +def _async_register(cb) -> int: + global _async_next_token + with _async_lock: + _async_next_token += 1 + _token = _async_next_token + _async_pending[_token] = cb + return _token + + +# Registered listener trampolines, keyed by subscription id. Holding +# the ctypes function objects here keeps them alive until unregistered; +# without this the GC could collect a trampoline the producer still calls. +_listener_refs: Dict[int, object] = {} + + +# === Module: edge_cases === + +class EdgeErrorsError(WeaveFFIError): + """Base exception for the `edge_cases` module's error domain.""" + + +class Busy(EdgeErrorsError): + """Resource is busy""" + + CODE = 7 + + def __init__(self, message: str = "Resource is busy") -> None: + super().__init__(7, message) + + +EdgeErrorsError.Busy = Busy + + +_EDGE_ERRORS_ERROR_CODES: Dict[int, type] = { + 7: Busy, +} + + +def _edge_errors_error_from(code: int, message: str, payload: bytes = b"") -> WeaveFFIError: + """Build the EdgeErrorsError subclass matching `code`, or a generic + WeaveFFIError for codes outside the domain (panics, marshalling).""" + _cls = _EDGE_ERRORS_ERROR_CODES.get(code) + if _cls is None: + return WeaveFFIError(code, message) + _exc = _cls(message) if message else _cls() + return _exc + + +def _check_edge_errors_error(err: _WeaveFFIErrorStruct) -> None: + if err.code != 0: + code = err.code + message = err.message.decode("utf-8") if err.message else "" + payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" + _lib.weaveffi_error_clear(ctypes.byref(err)) + raise _edge_errors_error_from(code, message, payload) + + +class Legacy(IntEnum): + """Deprecated enum with non-contiguous values and keyword-like variants""" + None_ = 0 + Default = 5 + Class = 10 + + +class Mode(IntEnum): + Fast = 1 + Safe = 2 + + +@dataclass +class Leaf: + weight: float + + +def _write_Leaf(_w: _BufferWriter, value: "Leaf") -> None: + _w.write_f32(value.weight) + + +def _read_Leaf(_r: _BufferReader) -> "Leaf": + return Leaf( + weight=_r.read_f32(), + ) + + +def _pack_Leaf(value: "Leaf") -> bytes: + _w = _BufferWriter() + _write_Leaf(_w, value) + return _w.finish() + + +def _unpack_Leaf(data: bytes) -> "Leaf": + return _decode_buffer(data, _read_Leaf) + + +@dataclass +class Inner: + """Middle layer of a three-deep struct nesting""" + + leaf: "Leaf" + leaves: List["Leaf"] + + +def _write_Inner(_w: _BufferWriter, value: "Inner") -> None: + _write_Leaf(_w, value.leaf) + _w.write_len(len(value.leaves)) + for _e0 in value.leaves: + _write_Leaf(_w, _e0) + + +def _read_Inner(_r: _BufferReader) -> "Inner": + return Inner( + leaf=_read_Leaf(_r), + leaves=[_read_Leaf(_r) for _i0 in range(_r.read_len())], + ) + + +def _pack_Inner(value: "Inner") -> bytes: + _w = _BufferWriter() + _write_Inner(_w, value) + return _w.finish() + + +def _unpack_Inner(data: bytes) -> "Inner": + return _decode_buffer(data, _read_Inner) + + +@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: _BufferWriter, value: "Outer") -> None: + _write_Inner(_w, value.inner) + if value.maybe_inner is None: + _w.write_option_flag(False) + else: + _w.write_option_flag(True) + _write_Inner(_w, value.maybe_inner) + _w.write_len(len(value.by_name)) + for _k0, _v0 in value.by_name.items(): + _w.write_string(_k0) + _write_Inner(_w, _v0) + _w.write_len(len(value.modes)) + for _e0 in value.modes: + _w.write_i32(_e0) + _w.write_len(len(value.matrix)) + for _e0 in value.matrix: + _w.write_len(len(_e0)) + for _e1 in _e0: + _w.write_i32(_e1) + _w.write_len(len(value.sparse)) + for _e0 in value.sparse: + if _e0 is None: + _w.write_option_flag(False) + else: + _w.write_option_flag(True) + _w.write_i64(_e0) + if value.blob is None: + _w.write_option_flag(False) + else: + _w.write_option_flag(True) + _w.write_bytes(value.blob) + if value.note is None: + _w.write_option_flag(False) + else: + _w.write_option_flag(True) + _w.write_string(value.note) + _w.write_len(len(value.by_id)) + for _k0, _v0 in value.by_id.items(): + _w.write_i64(_k0) + _w.write_string(_v0) + _w.write_bool(value._hidden) + _w.write_u8(value.v2) + + +def _read_Outer(_r: _BufferReader) -> "Outer": + return Outer( + inner=_read_Inner(_r), + maybe_inner=(_read_Inner(_r) if _r.read_option_flag() else None), + by_name=dict((_r.read_string(), _read_Inner(_r)) for _i0 in range(_r.read_len())), + modes=[Mode(_r.read_i32()) for _i0 in range(_r.read_len())], + matrix=[[_r.read_i32() for _i1 in range(_r.read_len())] for _i0 in range(_r.read_len())], + sparse=[(_r.read_i64() if _r.read_option_flag() else None) for _i0 in range(_r.read_len())], + blob=(_r.read_bytes() if _r.read_option_flag() else None), + note=(_r.read_string() if _r.read_option_flag() else None), + by_id=dict((_r.read_i64(), _r.read_string()) for _i0 in range(_r.read_len())), + _hidden=_r.read_bool(), + v2=_r.read_u8(), + ) + + +def _pack_Outer(value: "Outer") -> bytes: + _w = _BufferWriter() + _write_Outer(_w, value) + return _w.finish() + + +def _unpack_Outer(data: bytes) -> "Outer": + return _decode_buffer(data, _read_Outer) + + +@dataclass +class Keywords: + """Every field is a reserved word somewhere""" + + 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: int + + +def _write_Keywords(_w: _BufferWriter, 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: _BufferReader) -> "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(), + ) + + +def _pack_Keywords(value: "Keywords") -> bytes: + _w = _BufferWriter() + _write_Keywords(_w, value) + return _w.finish() + + +def _unpack_Keywords(data: bytes) -> "Keywords": + return _decode_buffer(data, _read_Keywords) + + +@dataclass +class Retired: + """Deprecated struct""" + + id: int + + +def _write_Retired(_w: _BufferWriter, value: "Retired") -> None: + _w.write_i64(value.id) + + +def _read_Retired(_r: _BufferReader) -> "Retired": + return Retired( + id=_r.read_i64(), + ) + + +def _pack_Retired(value: "Retired") -> bytes: + _w = _BufferWriter() + _write_Retired(_w, value) + return _w.finish() + + +def _unpack_Retired(data: bytes) -> "Retired": + return _decode_buffer(data, _read_Retired) + + +class Widget: + """Interface with no constructor; instances come from make_widget""" + + @classmethod + def _from_ptr(cls, ptr) -> "Widget": + _obj = cls.__new__(cls) + _obj._ptr = ptr + return _obj + + def __init__(self, _ptr: int) -> None: + self._ptr = _ptr + + + def __del__(self) -> None: + if self._ptr is not None: + _lib.weaveffi_edge_cases_Widget_destroy.argtypes = [ctypes.c_void_p] + _lib.weaveffi_edge_cases_Widget_destroy.restype = None + _lib.weaveffi_edge_cases_Widget_destroy(self._ptr) + self._ptr = None + + def refresh(self) -> None: + """Void method that can fail + + Raises + ------ + EdgeErrorsError + If the call reports one of the domain's error codes. + """ + _fn = _lib.weaveffi_edge_cases_Widget_refresh + _fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = None + _err = _WeaveFFIErrorStruct() + _fn(self._ptr, ctypes.byref(_err)) + _check_edge_errors_error(_err) + + + def rename(self, class_: str, import_: Optional[str]) -> str: + _fn = _lib.weaveffi_edge_cases_Widget_rename + _fn.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_void_p + _import__w = _BufferWriter() + if import_ is None: + _import__w.write_option_flag(False) + else: + _import__w.write_option_flag(True) + _import__w.write_string(import_) + _import__buf = _import__w.finish() + _err = _WeaveFFIErrorStruct() + _result = _fn(self._ptr, _string_to_bytes(class_), _import__buf, len(_import__buf), ctypes.byref(_err)) + _check_error(_err) + return _take_string(_result) or "" + + + 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. + """ + _fn = _lib.weaveffi_edge_cases_Widget_fetch_async + _loop = asyncio.get_running_loop() + _fut = _loop.create_future() + + def _cb_impl(context, err, result_ptr, result_len): + # Fires exactly once, on a producer thread: take ownership of + # the result here, then hop back to the event loop. + _state = {"err": None, "val": None} + if err and err.contents.code != 0: + _code = err.contents.code + _msg = err.contents.message.decode("utf-8") if err.contents.message else "" + _payload = ctypes.string_at(err.contents.payload_ptr, err.contents.payload_len) if err.contents.payload_ptr else b"" + _lib.weaveffi_error_free(err) + _state["err"] = _edge_errors_error_from(_code, _msg, _payload) + else: + try: + _state["val"] = _unpack_Outer(_take_buffer(ctypes.cast(result_ptr, ctypes.c_void_p).value, result_len)) + except Exception as _exc: + _state["err"] = _exc + + def _resolve(): + _async_pending.pop(_token, None) + # A cancelled future must not be resolved. + if _fut.cancelled(): + return + if _state["err"] is not None: + _fut.set_exception(_state["err"]) + else: + _fut.set_result(_state["val"]) + + _loop.call_soon_threadsafe(_resolve) + + _cb_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct), ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t) + _cb = _cb_type(_cb_impl) + _token = _async_register(_cb) # pinned until completion + _fn.argtypes = [ctypes.c_void_p, ctypes.c_int64, _cb_type, ctypes.c_void_p] + _fn.restype = None + _fn(self._ptr, id, _cb, None) + return await _fut + + + def old_name(self) -> str: + import warnings + warnings.warn("Use rename instead", DeprecationWarning, stacklevel=2) + _fn = _lib.weaveffi_edge_cases_Widget_old_name + _fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_void_p + _err = _WeaveFFIErrorStruct() + _result = _fn(self._ptr, ctypes.byref(_err)) + _check_error(_err) + return _take_string(_result) or "" + + + +class Vault: + """Deprecated interface with a throwing constructor""" + + @classmethod + def _from_ptr(cls, ptr) -> "Vault": + _obj = cls.__new__(cls) + _obj._ptr = ptr + return _obj + + def __init__(self, _ptr: int) -> None: + self._ptr = _ptr + + + def __del__(self) -> None: + if self._ptr is not None: + _lib.weaveffi_edge_cases_Vault_destroy.argtypes = [ctypes.c_void_p] + _lib.weaveffi_edge_cases_Vault_destroy.restype = None + _lib.weaveffi_edge_cases_Vault_destroy(self._ptr) + self._ptr = None + + @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. + """ + _fn = _lib.weaveffi_edge_cases_Vault_open + _fn.argtypes = [ctypes.c_char_p, ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_void_p + _err = _WeaveFFIErrorStruct() + _result = _fn(_string_to_bytes(path), ctypes.byref(_err)) + _check_edge_errors_error(_err) + if _result is None: + raise WeaveFFIError(-1, "null pointer") + return cls._from_ptr(_result) + + + def peek(self) -> Optional["Widget"]: + _fn = _lib.weaveffi_edge_cases_Vault_peek + _fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_void_p + _err = _WeaveFFIErrorStruct() + _result = _fn(self._ptr, ctypes.byref(_err)) + _check_error(_err) + if _result is None: + return None + return Widget._from_ptr(_result) + + + @staticmethod + def probe() -> bool: + """Throwing static + + Raises + ------ + EdgeErrorsError + If the call reports one of the domain's error codes. + """ + _fn = _lib.weaveffi_edge_cases_Vault_probe + _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_int32 + _err = _WeaveFFIErrorStruct() + _result = _fn(ctypes.byref(_err)) + _check_edge_errors_error(_err) + return bool(_result) + + + +# Callback carrying composite payloads +# Callback type OnBatch: Callable[[List["Outer"], bytes, Optional["Outer"], "Mode"], None] +_CFUNC_weaveffi_edge_cases_OnBatch_fn = ctypes.CFUNCTYPE(None, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, ctypes.c_int32, ctypes.c_void_p) + + +# Callback type OnTick: Callable[[], None] +_CFUNC_weaveffi_edge_cases_OnTick_fn = ctypes.CFUNCTYPE(None, ctypes.c_void_p) + + +def register_batch_listener(callback: Callable[[List["Outer"], bytes, Optional["Outer"], "Mode"], None]) -> int: + """Register a OnBatch listener. Returns a subscription id for unregister_batch_listener().""" + def _trampoline(items_ptr, items_len, raw_ptr, raw_len, first_ptr, first_len, mode, _context): + callback(_decode_buffer(ctypes.string_at(items_ptr, items_len) if items_ptr else b"", lambda _r: [_read_Outer(_r) for _i0 in range(_r.read_len())]), bytes(raw_ptr[:raw_len]) if raw_ptr else b"", _decode_buffer(ctypes.string_at(first_ptr, first_len) if first_ptr else b"", lambda _r: (_read_Outer(_r) if _r.read_option_flag() else None)), Mode(mode)) + _cfunc = _CFUNC_weaveffi_edge_cases_OnBatch_fn(_trampoline) + _fn = _lib.weaveffi_edge_cases_register_batch_listener + _fn.argtypes = [_CFUNC_weaveffi_edge_cases_OnBatch_fn, ctypes.c_void_p] + _fn.restype = ctypes.c_uint64 + _listener_id = int(_fn(_cfunc, None)) + _listener_refs[_listener_id] = _cfunc + return _listener_id + + +def unregister_batch_listener(listener_id: int) -> None: + """Unregister a listener previously registered with register_batch_listener().""" + _fn = _lib.weaveffi_edge_cases_unregister_batch_listener + _fn.argtypes = [ctypes.c_uint64] + _fn.restype = None + _fn(ctypes.c_uint64(listener_id)) + _listener_refs.pop(listener_id, None) + + +def register_tick_listener(callback: Callable[[], None]) -> int: + """Register a OnTick listener. Returns a subscription id for unregister_tick_listener().""" + def _trampoline(_context): + callback() + _cfunc = _CFUNC_weaveffi_edge_cases_OnTick_fn(_trampoline) + _fn = _lib.weaveffi_edge_cases_register_tick_listener + _fn.argtypes = [_CFUNC_weaveffi_edge_cases_OnTick_fn, ctypes.c_void_p] + _fn.restype = ctypes.c_uint64 + _listener_id = int(_fn(_cfunc, None)) + _listener_refs[_listener_id] = _cfunc + return _listener_id + + +def unregister_tick_listener(listener_id: int) -> None: + """Unregister a listener previously registered with register_tick_listener().""" + _fn = _lib.weaveffi_edge_cases_unregister_tick_listener + _fn.argtypes = [ctypes.c_uint64] + _fn.restype = None + _fn(ctypes.c_uint64(listener_id)) + _listener_refs.pop(listener_id, None) + + +def noop() -> None: + """No parameters and no result + """ + _fn = _lib.weaveffi_edge_cases_noop + _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = None + _err = _WeaveFFIErrorStruct() + _fn(ctypes.byref(_err)) + _check_error(_err) + + +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 + """ + _fn = _lib.weaveffi_edge_cases_select + _fn.argtypes = [ctypes.c_int32, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_int32, ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_int32 + _func_w = _BufferWriter() + _func_w.write_len(len(func)) + for _e0 in func: + _func_w.write_string(_e0) + _func_buf = _func_w.finish() + _range_w = _BufferWriter() + if range is None: + _range_w.write_option_flag(False) + else: + _range_w.write_option_flag(True) + _range_w.write_i32(range) + _range_buf = _range_w.finish() + _err = _WeaveFFIErrorStruct() + _result = _fn(class_, _string_to_bytes(from_), _func_buf, len(_func_buf), _range_buf, len(_range_buf), 1 if type else 0, ctypes.byref(_err)) + _check_error(_err) + return _result + + +def nested_optional(items: Optional[List[Optional["Outer"]]]) -> Optional[List[Optional["Outer"]]]: + """Optional list of optional structs, the deepest legal nesting + """ + _fn = _lib.weaveffi_edge_cases_nested_optional + _fn.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_void_p + _items_w = _BufferWriter() + if items is None: + _items_w.write_option_flag(False) + else: + _items_w.write_option_flag(True) + _items_w.write_len(len(items)) + for _e0 in items: + if _e0 is None: + _items_w.write_option_flag(False) + else: + _items_w.write_option_flag(True) + _write_Outer(_items_w, _e0) + _items_buf = _items_w.finish() + _err = _WeaveFFIErrorStruct() + _out_len = ctypes.c_size_t(0) + _result = _fn(_items_buf, len(_items_buf), ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + _data = _take_buffer(_result, _out_len.value) + return _decode_buffer(_data, lambda _r: ([(_read_Outer(_r) if _r.read_option_flag() else None) for _i0 in range(_r.read_len())] if _r.read_option_flag() else None)) + + +def optional_params(text: Optional[str], count: Optional[int], outer: Optional["Outer"], tags: Optional[List[str]], blob: Optional[bytes], mode: Optional["Mode"]) -> Optional["Mode"]: + _fn = _lib.weaveffi_edge_cases_optional_params + _fn.argtypes = [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(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_void_p + _text_w = _BufferWriter() + if text is None: + _text_w.write_option_flag(False) + else: + _text_w.write_option_flag(True) + _text_w.write_string(text) + _text_buf = _text_w.finish() + _count_w = _BufferWriter() + if count is None: + _count_w.write_option_flag(False) + else: + _count_w.write_option_flag(True) + _count_w.write_i32(count) + _count_buf = _count_w.finish() + _outer_w = _BufferWriter() + if outer is None: + _outer_w.write_option_flag(False) + else: + _outer_w.write_option_flag(True) + _write_Outer(_outer_w, outer) + _outer_buf = _outer_w.finish() + _tags_w = _BufferWriter() + if tags is None: + _tags_w.write_option_flag(False) + else: + _tags_w.write_option_flag(True) + _tags_w.write_len(len(tags)) + for _e0 in tags: + _tags_w.write_string(_e0) + _tags_buf = _tags_w.finish() + _blob_w = _BufferWriter() + if blob is None: + _blob_w.write_option_flag(False) + else: + _blob_w.write_option_flag(True) + _blob_w.write_bytes(blob) + _blob_buf = _blob_w.finish() + _mode_w = _BufferWriter() + if mode is None: + _mode_w.write_option_flag(False) + else: + _mode_w.write_option_flag(True) + _mode_w.write_i32(mode) + _mode_buf = _mode_w.finish() + _err = _WeaveFFIErrorStruct() + _out_len = ctypes.c_size_t(0) + _result = _fn(_text_buf, len(_text_buf), _count_buf, len(_count_buf), _outer_buf, len(_outer_buf), _tags_buf, len(_tags_buf), _blob_buf, len(_blob_buf), _mode_buf, len(_mode_buf), ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + _data = _take_buffer(_result, _out_len.value) + return _decode_buffer(_data, lambda _r: (Mode(_r.read_i32()) if _r.read_option_flag() else None)) + + +def mutable_params(text: str, blob: bytes) -> None: + """Write-back parameters + """ + _fn = _lib.weaveffi_edge_cases_mutable_params + _fn.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = None + _blob_arr = (ctypes.c_uint8 * len(blob))(*blob) + _err = _WeaveFFIErrorStruct() + _fn(_string_to_bytes(text), _blob_arr, len(blob), ctypes.byref(_err)) + _check_error(_err) + + +def make_widget(label: str) -> "Widget": + """Factory for an interface with no constructor + """ + _fn = _lib.weaveffi_edge_cases_make_widget + _fn.argtypes = [ctypes.c_char_p, ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_void_p + _err = _WeaveFFIErrorStruct() + _result = _fn(_string_to_bytes(label), ctypes.byref(_err)) + _check_error(_err) + if _result is None: + raise WeaveFFIError(-1, "null pointer") + return Widget._from_ptr(_result) + + +def find_widget(id: int) -> Optional["Widget"]: + """Optional interface return + + Raises + ------ + EdgeErrorsError + If the call reports one of the domain's error codes. + """ + _fn = _lib.weaveffi_edge_cases_find_widget + _fn.argtypes = [ctypes.c_int64, ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_void_p + _err = _WeaveFFIErrorStruct() + _result = _fn(id, ctypes.byref(_err)) + _check_edge_errors_error(_err) + if _result is None: + return None + return Widget._from_ptr(_result) + + +def inspect_widget(widget: "Widget", fallback: Optional["Widget"]) -> str: + """Interface parameter, including an optional one + """ + _fn = _lib.weaveffi_edge_cases_inspect_widget + _fn.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_void_p + _err = _WeaveFFIErrorStruct() + _result = _fn(widget._ptr, fallback._ptr if fallback is not None else None, ctypes.byref(_err)) + _check_error(_err) + return _take_string(_result) or "" + + +def typed_handle_param(token: int, raw: int) -> int: + _fn = _lib.weaveffi_edge_cases_typed_handle_param + _fn.argtypes = [ctypes.c_void_p, ctypes.c_uint64, ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_void_p + _err = _WeaveFFIErrorStruct() + _result = _fn(token, raw, ctypes.byref(_err)) + _check_error(_err) + return _result or 0 + + +class _StringStreamIterator: + """Lazy iterator over a producer stream: each step pulls one element + across the C boundary. The native handle is released exactly once, on + exhaustion, on close(), or when the iterator is garbage collected.""" + + def __init__(self, ptr): + self._ptr = ptr + self._done = False + + def __iter__(self): + return self + + def __next__(self): + if self._done: + raise StopIteration + _next_fn = _lib.weaveffi_edge_cases_StringStreamIterator_next + _next_fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_WeaveFFIErrorStruct)] + _next_fn.restype = ctypes.c_int32 + _out_item = ctypes.c_void_p() + _err = _WeaveFFIErrorStruct() + _has = _next_fn(self._ptr, ctypes.byref(_out_item), ctypes.byref(_err)) + _check_error(_err) + if not _has: + self._done = True + self._destroy() + raise StopIteration + return _take_string(_out_item.value) + + def close(self): + """Release the native iterator without draining it.""" + self._done = True + self._destroy() + + def _destroy(self): + if self._ptr is not None: + _destroy_fn = _lib.weaveffi_edge_cases_StringStreamIterator_destroy + _destroy_fn.argtypes = [ctypes.c_void_p] + _destroy_fn.restype = None + _destroy_fn(self._ptr) + self._ptr = None + + def __del__(self): + self._destroy() + + +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). + """ + _fn = _lib.weaveffi_edge_cases_string_stream + _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_void_p + _err = _WeaveFFIErrorStruct() + _result = _fn(ctypes.byref(_err)) + _check_error(_err) + return _StringStreamIterator(_result) + + +class _ScalarStreamIterator: + """Lazy iterator over a producer stream: each step pulls one element + across the C boundary. The native handle is released exactly once, on + exhaustion, on close(), or when the iterator is garbage collected.""" + + def __init__(self, ptr): + self._ptr = ptr + self._done = False + + def __iter__(self): + return self + + def __next__(self): + if self._done: + raise StopIteration + _next_fn = _lib.weaveffi_edge_cases_ScalarStreamIterator_next + _next_fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_int32), ctypes.POINTER(_WeaveFFIErrorStruct)] + _next_fn.restype = ctypes.c_int32 + _out_item = ctypes.c_int32() + _err = _WeaveFFIErrorStruct() + _has = _next_fn(self._ptr, ctypes.byref(_out_item), ctypes.byref(_err)) + _check_error(_err) + if not _has: + self._done = True + self._destroy() + raise StopIteration + return _out_item.value + + def close(self): + """Release the native iterator without draining it.""" + self._done = True + self._destroy() + + def _destroy(self): + if self._ptr is not None: + _destroy_fn = _lib.weaveffi_edge_cases_ScalarStreamIterator_destroy + _destroy_fn.argtypes = [ctypes.c_void_p] + _destroy_fn.restype = None + _destroy_fn(self._ptr) + self._ptr = None + + def __del__(self): + self._destroy() + + +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). + """ + _fn = _lib.weaveffi_edge_cases_scalar_stream + _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_void_p + _err = _WeaveFFIErrorStruct() + _result = _fn(ctypes.byref(_err)) + _check_error(_err) + return _ScalarStreamIterator(_result) + + +class _ModeStreamIterator: + """Lazy iterator over a producer stream: each step pulls one element + across the C boundary. The native handle is released exactly once, on + exhaustion, on close(), or when the iterator is garbage collected.""" + + def __init__(self, ptr): + self._ptr = ptr + self._done = False + + def __iter__(self): + return self + + def __next__(self): + if self._done: + raise StopIteration + _next_fn = _lib.weaveffi_edge_cases_ModeStreamIterator_next + _next_fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_int32), ctypes.POINTER(_WeaveFFIErrorStruct)] + _next_fn.restype = ctypes.c_int32 + _out_item = ctypes.c_int32() + _err = _WeaveFFIErrorStruct() + _has = _next_fn(self._ptr, ctypes.byref(_out_item), ctypes.byref(_err)) + _check_error(_err) + if not _has: + self._done = True + self._destroy() + raise StopIteration + return Mode(_out_item.value) + + def close(self): + """Release the native iterator without draining it.""" + self._done = True + self._destroy() + + def _destroy(self): + if self._ptr is not None: + _destroy_fn = _lib.weaveffi_edge_cases_ModeStreamIterator_destroy + _destroy_fn.argtypes = [ctypes.c_void_p] + _destroy_fn.restype = None + _destroy_fn(self._ptr) + self._ptr = None + + def __del__(self): + self._destroy() + + +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). + """ + _fn = _lib.weaveffi_edge_cases_mode_stream + _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_void_p + _err = _WeaveFFIErrorStruct() + _result = _fn(ctypes.byref(_err)) + _check_error(_err) + return _ModeStreamIterator(_result) + + +async def async_void() -> None: + """Async with no result + """ + _fn = _lib.weaveffi_edge_cases_async_void_async + _loop = asyncio.get_running_loop() + _fut = _loop.create_future() + + def _cb_impl(context, err): + # Fires exactly once, on a producer thread: take ownership of + # the result here, then hop back to the event loop. + _state = {"err": None, "val": None} + if err and err.contents.code != 0: + _code = err.contents.code + _msg = err.contents.message.decode("utf-8") if err.contents.message else "" + _lib.weaveffi_error_free(err) + _state["err"] = WeaveFFIError(_code, _msg) + else: + try: + _state["val"] = None + except Exception as _exc: + _state["err"] = _exc + + def _resolve(): + _async_pending.pop(_token, None) + # A cancelled future must not be resolved. + if _fut.cancelled(): + return + if _state["err"] is not None: + _fut.set_exception(_state["err"]) + else: + _fut.set_result(_state["val"]) + + _loop.call_soon_threadsafe(_resolve) + + _cb_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct)) + _cb = _cb_type(_cb_impl) + _token = _async_register(_cb) # pinned until completion + _fn.argtypes = [_cb_type, ctypes.c_void_p] + _fn.restype = None + _fn(_cb, None) + await _fut + + +async def async_bytes(size: int) -> bytes: + _fn = _lib.weaveffi_edge_cases_async_bytes_async + _loop = asyncio.get_running_loop() + _fut = _loop.create_future() + + def _cb_impl(context, err, result, result_len): + # Fires exactly once, on a producer thread: take ownership of + # the result here, then hop back to the event loop. + _state = {"err": None, "val": None} + if err and err.contents.code != 0: + _code = err.contents.code + _msg = err.contents.message.decode("utf-8") if err.contents.message else "" + _lib.weaveffi_error_free(err) + _state["err"] = WeaveFFIError(_code, _msg) + else: + try: + if not result: + _state["val"] = b"" + else: + _n = int(result_len) + _state["val"] = _take_buffer(ctypes.cast(result, ctypes.c_void_p).value, _n) + except Exception as _exc: + _state["err"] = _exc + + def _resolve(): + _async_pending.pop(_token, None) + # A cancelled future must not be resolved. + if _fut.cancelled(): + return + if _state["err"] is not None: + _fut.set_exception(_state["err"]) + else: + _fut.set_result(_state["val"]) + + _loop.call_soon_threadsafe(_resolve) + + _cb_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct), ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t) + _cb = _cb_type(_cb_impl) + _token = _async_register(_cb) # pinned until completion + _fn.argtypes = [ctypes.c_uint32, _cb_type, ctypes.c_void_p] + _fn.restype = None + _fn(size, _cb, None) + return await _fut + + +async def async_struct() -> "Outer": + """ + Raises + ------ + EdgeErrorsError + If the call reports one of the domain's error codes. + """ + _fn = _lib.weaveffi_edge_cases_async_struct_async + _loop = asyncio.get_running_loop() + _fut = _loop.create_future() + + def _cb_impl(context, err, result_ptr, result_len): + # Fires exactly once, on a producer thread: take ownership of + # the result here, then hop back to the event loop. + _state = {"err": None, "val": None} + if err and err.contents.code != 0: + _code = err.contents.code + _msg = err.contents.message.decode("utf-8") if err.contents.message else "" + _payload = ctypes.string_at(err.contents.payload_ptr, err.contents.payload_len) if err.contents.payload_ptr else b"" + _lib.weaveffi_error_free(err) + _state["err"] = _edge_errors_error_from(_code, _msg, _payload) + else: + try: + _state["val"] = _unpack_Outer(_take_buffer(ctypes.cast(result_ptr, ctypes.c_void_p).value, result_len)) + except Exception as _exc: + _state["err"] = _exc + + def _resolve(): + _async_pending.pop(_token, None) + # A cancelled future must not be resolved. + if _fut.cancelled(): + return + if _state["err"] is not None: + _fut.set_exception(_state["err"]) + else: + _fut.set_result(_state["val"]) + + _loop.call_soon_threadsafe(_resolve) + + _cb_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct), ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t) + _cb = _cb_type(_cb_impl) + _token = _async_register(_cb) # pinned until completion + _fn.argtypes = [_cb_type, ctypes.c_void_p] + _fn.restype = None + _fn(_cb, None) + return await _fut + + +async def async_list() -> List["Outer"]: + """ + Raises + ------ + EdgeErrorsError + If the call reports one of the domain's error codes. + """ + _fn = _lib.weaveffi_edge_cases_async_list_async + _loop = asyncio.get_running_loop() + _fut = _loop.create_future() + + def _cb_impl(context, err, result_ptr, result_len): + # Fires exactly once, on a producer thread: take ownership of + # the result here, then hop back to the event loop. + _state = {"err": None, "val": None} + if err and err.contents.code != 0: + _code = err.contents.code + _msg = err.contents.message.decode("utf-8") if err.contents.message else "" + _payload = ctypes.string_at(err.contents.payload_ptr, err.contents.payload_len) if err.contents.payload_ptr else b"" + _lib.weaveffi_error_free(err) + _state["err"] = _edge_errors_error_from(_code, _msg, _payload) + else: + try: + _state["val"] = _decode_buffer(_take_buffer(ctypes.cast(result_ptr, ctypes.c_void_p).value, result_len), lambda _r: [_read_Outer(_r) for _i0 in range(_r.read_len())]) + except Exception as _exc: + _state["err"] = _exc + + def _resolve(): + _async_pending.pop(_token, None) + # A cancelled future must not be resolved. + if _fut.cancelled(): + return + if _state["err"] is not None: + _fut.set_exception(_state["err"]) + else: + _fut.set_result(_state["val"]) + + _loop.call_soon_threadsafe(_resolve) + + _cb_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct), ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t) + _cb = _cb_type(_cb_impl) + _token = _async_register(_cb) # pinned until completion + _fn.argtypes = [ctypes.c_void_p, _cb_type, ctypes.c_void_p] + _fn.restype = None + _fn(None, _cb, None) + return await _fut + + +async def async_optional() -> Optional[int]: + _fn = _lib.weaveffi_edge_cases_async_optional_async + _loop = asyncio.get_running_loop() + _fut = _loop.create_future() + + def _cb_impl(context, err, result_ptr, result_len): + # Fires exactly once, on a producer thread: take ownership of + # the result here, then hop back to the event loop. + _state = {"err": None, "val": None} + if err and err.contents.code != 0: + _code = err.contents.code + _msg = err.contents.message.decode("utf-8") if err.contents.message else "" + _lib.weaveffi_error_free(err) + _state["err"] = WeaveFFIError(_code, _msg) + else: + try: + _state["val"] = _decode_buffer(_take_buffer(ctypes.cast(result_ptr, ctypes.c_void_p).value, result_len), lambda _r: (_r.read_i64() if _r.read_option_flag() else None)) + except Exception as _exc: + _state["err"] = _exc + + def _resolve(): + _async_pending.pop(_token, None) + # A cancelled future must not be resolved. + if _fut.cancelled(): + return + if _state["err"] is not None: + _fut.set_exception(_state["err"]) + else: + _fut.set_result(_state["val"]) + + _loop.call_soon_threadsafe(_resolve) + + _cb_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct), ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t) + _cb = _cb_type(_cb_impl) + _token = _async_register(_cb) # pinned until completion + _fn.argtypes = [_cb_type, ctypes.c_void_p] + _fn.restype = None + _fn(_cb, None) + return await _fut + + +async def async_enum() -> "Mode": + _fn = _lib.weaveffi_edge_cases_async_enum_async + _loop = asyncio.get_running_loop() + _fut = _loop.create_future() + + def _cb_impl(context, err, result): + # Fires exactly once, on a producer thread: take ownership of + # the result here, then hop back to the event loop. + _state = {"err": None, "val": None} + if err and err.contents.code != 0: + _code = err.contents.code + _msg = err.contents.message.decode("utf-8") if err.contents.message else "" + _lib.weaveffi_error_free(err) + _state["err"] = WeaveFFIError(_code, _msg) + else: + try: + _state["val"] = Mode(result) + except Exception as _exc: + _state["err"] = _exc + + def _resolve(): + _async_pending.pop(_token, None) + # A cancelled future must not be resolved. + if _fut.cancelled(): + return + if _state["err"] is not None: + _fut.set_exception(_state["err"]) + else: + _fut.set_result(_state["val"]) + + _loop.call_soon_threadsafe(_resolve) + + _cb_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct), ctypes.c_int32) + _cb = _cb_type(_cb_impl) + _token = _async_register(_cb) # pinned until completion + _fn.argtypes = [_cb_type, ctypes.c_void_p] + _fn.restype = None + _fn(_cb, None) + return await _fut + + +async def async_keywords(class_: str, import_: int) -> str: + _fn = _lib.weaveffi_edge_cases_async_keywords_async + _loop = asyncio.get_running_loop() + _fut = _loop.create_future() + + def _cb_impl(context, err, result): + # Fires exactly once, on a producer thread: take ownership of + # the result here, then hop back to the event loop. + _state = {"err": None, "val": None} + if err and err.contents.code != 0: + _code = err.contents.code + _msg = err.contents.message.decode("utf-8") if err.contents.message else "" + _lib.weaveffi_error_free(err) + _state["err"] = WeaveFFIError(_code, _msg) + else: + try: + _state["val"] = _take_string(result) or "" + except Exception as _exc: + _state["err"] = _exc + + def _resolve(): + _async_pending.pop(_token, None) + # A cancelled future must not be resolved. + if _fut.cancelled(): + return + if _state["err"] is not None: + _fut.set_exception(_state["err"]) + else: + _fut.set_result(_state["val"]) + + _loop.call_soon_threadsafe(_resolve) + + _cb_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct), ctypes.c_void_p) + _cb = _cb_type(_cb_impl) + _token = _async_register(_cb) # pinned until completion + _fn.argtypes = [ctypes.c_char_p, ctypes.c_int32, _cb_type, ctypes.c_void_p] + _fn.restype = None + _fn(_string_to_bytes(class_), import_, _cb, None) + return await _fut + + +def keywords_roundtrip(value: "Keywords") -> "Keywords": + _fn = _lib.weaveffi_edge_cases_keywords_roundtrip + _fn.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_void_p + _value_buf = _pack_Keywords(value) + _err = _WeaveFFIErrorStruct() + _out_len = ctypes.c_size_t(0) + _result = _fn(_value_buf, len(_value_buf), ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + _data = _take_buffer(_result, _out_len.value) + return _unpack_Keywords(_data) + + +def legacy_value(value: "Legacy") -> "Retired": + import warnings + warnings.warn("Use Mode-based APIs", DeprecationWarning, stacklevel=2) + _fn = _lib.weaveffi_edge_cases_legacy_value + _fn.argtypes = [ctypes.c_int32, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] + _fn.restype = ctypes.c_void_p + _err = _WeaveFFIErrorStruct() + _out_len = ctypes.c_size_t(0) + _result = _fn(value.value, ctypes.byref(_out_len), ctypes.byref(_err)) + _check_error(_err) + _data = _take_buffer(_result, _out_len.value) + return _unpack_Retired(_data) + +# END weaveffi.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_edge_cases__weaveffi_weaveffi_pyi.snap b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__weaveffi_weaveffi_pyi.snap new file mode 100644 index 00000000..d3f7cc79 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/python_edge_cases__weaveffi_weaveffi_pyi.snap @@ -0,0 +1,200 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from weaveffi.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate weaveffi.yml -o + +from enum import IntEnum +from typing import Callable, Dict, Iterator, List, Optional, Type + +class WeaveFFIError(Exception): + code: int + message: str + def __init__(self, code: int, message: str) -> None: ... + +class EdgeErrorsError(WeaveFFIError): + Busy: Type["Busy"] + def __init__(self, code: int, message: str) -> None: ... + +class Busy(EdgeErrorsError): + CODE: int + def __init__(self, message: str = ...) -> None: ... + +# Deprecated enum with non-contiguous values and keyword-like variants +class Legacy(IntEnum): + None_: int + Default: int + Class: int + +class Mode(IntEnum): + Fast: int + Safe: int + +class Leaf: + weight: float + def __init__(self, weight: float) -> None: ... + +# Middle layer of a three-deep struct nesting +class Inner: + leaf: "Leaf" + leaves: List["Leaf"] + def __init__(self, leaf: "Leaf", leaves: List["Leaf"]) -> None: ... + +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 __init__(self, 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) -> None: ... + +# Every field is a reserved word somewhere +class Keywords: + 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: int + def __init__(self, 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: int) -> None: ... + +# Deprecated struct +class Retired: + id: int + def __init__(self, id: int) -> None: ... + +# Interface with no constructor; instances come from make_widget +class Widget: + def refresh(self) -> None: ... + def rename(self, class_: str, import_: Optional[str]) -> str: ... + async def fetch(self, id: int) -> "Outer": ... + def old_name(self) -> str: ... + +# Deprecated interface with a throwing constructor +class Vault: + @classmethod + def open(cls, path: str) -> "Vault": ... + def peek(self) -> Optional["Widget"]: ... + @staticmethod + def probe() -> bool: ... + +def register_batch_listener(callback: Callable[[List["Outer"], bytes, Optional["Outer"], "Mode"], None]) -> int: ... +def unregister_batch_listener(listener_id: int) -> None: ... + +def register_tick_listener(callback: Callable[[], None]) -> int: ... +def unregister_tick_listener(listener_id: int) -> None: ... + +# No parameters and no result +def noop() -> None: ... + +# Function named after a keyword whose parameters are all keywords +def select(class_: int, from_: str, func: List[str], range: Optional[int], type: bool) -> int: ... + +# Optional list of optional structs, the deepest legal nesting +def nested_optional(items: Optional[List[Optional["Outer"]]]) -> Optional[List[Optional["Outer"]]]: ... + +def optional_params(text: Optional[str], count: Optional[int], outer: Optional["Outer"], tags: Optional[List[str]], blob: Optional[bytes], mode: Optional["Mode"]) -> Optional["Mode"]: ... + +# Write-back parameters +def mutable_params(text: str, blob: bytes) -> None: ... + +# Factory for an interface with no constructor +def make_widget(label: str) -> "Widget": ... + +# Optional interface return +def find_widget(id: int) -> Optional["Widget"]: ... + +# Interface parameter, including an optional one +def inspect_widget(widget: "Widget", fallback: Optional["Widget"]) -> str: ... + +def typed_handle_param(token: int, raw: int) -> int: ... + +# Iterator of strings +def string_stream() -> Iterator[str]: ... + +# Iterator of scalars +def scalar_stream() -> Iterator[int]: ... + +# Iterator of enums +def mode_stream() -> Iterator["Mode"]: ... + +# Async with no result +async def async_void() -> None: ... + +async def async_bytes(size: int) -> bytes: ... + +async def async_struct() -> "Outer": ... + +async def async_list() -> List["Outer"]: ... + +async def async_optional() -> Optional[int]: ... + +async def async_enum() -> "Mode": ... + +async def async_keywords(class_: str, import_: int) -> str: ... + +def keywords_roundtrip(value: "Keywords") -> "Keywords": ... + +def legacy_value(value: "Legacy") -> "Retired": ... + +# END weaveffi.pyi diff --git a/crates/weaveffi-cli/tests/snapshots/python_events__README_md.snap b/crates/weaveffi-cli/tests/snapshots/python_events__README_md.snap deleted file mode 100644 index e3b8a7b9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_events__README_md.snap +++ /dev/null @@ -1,35 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# weaveffi (Python) - -Auto-generated Python bindings using ctypes. - -## Prerequisites - -- Python >= 3.8 -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path. - -## Install - -```bash -pip install . -``` - -## Development install - -```bash -pip install -e . -``` - -## Usage - -```python -from weaveffi import * -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/python_events__pyproject_toml.snap b/crates/weaveffi-cli/tests/snapshots/python_events__pyproject_toml.snap deleted file mode 100644 index c07695fa..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_events__pyproject_toml.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "weaveffi" -version = "0.1.0" -description = "weaveffi bindings generated by WeaveFFI" -requires-python = ">=3.8" - -[tool.setuptools] -packages = ["weaveffi"] - -# END pyproject.toml diff --git a/crates/weaveffi-cli/tests/snapshots/python_events__setup_py.snap b/crates/weaveffi-cli/tests/snapshots/python_events__setup_py.snap deleted file mode 100644 index 4c5da6f5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_events__setup_py.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -from setuptools import setup - -setup( - name="weaveffi", - version="0.1.0", - packages=["weaveffi"], -) - -# END setup.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_events__weaveffi___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_events__weaveffi___init___py.snap deleted file mode 100644 index e4ae4ecc..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_events__weaveffi___init___py.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -from .weaveffi import * # noqa: F401,F403 - -# END __init__.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_events__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_events__weaveffi_weaveffi_py.snap deleted file mode 100644 index 0fd935eb..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_events__weaveffi_weaveffi_py.snap +++ /dev/null @@ -1,372 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -"""WeaveFFI Python ctypes bindings (auto-generated)""" -import contextlib -import ctypes -import os -import platform -import struct -from dataclasses import dataclass -from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional - - -class WeaveFFIError(Exception): - def __init__(self, code: int, message: str) -> None: - self.code = code - self.message = message - super().__init__(f"({code}) {message}") - - -class _WeaveFFIErrorStruct(ctypes.Structure): - _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 in WEAVEFFI_LIBRARY wins, so callers can point at a - # specific build artifact regardless of its file name or location. - override = os.environ.get("WEAVEFFI_LIBRARY") - if override: - return ctypes.CDLL(override) - system = platform.system() - if system == "Darwin": - name = "libweaveffi.dylib" - elif system == "Windows": - name = "weaveffi.dll" - else: - name = "libweaveffi.so" - return ctypes.CDLL(name) - - -_lib = _load_library() -_lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] -_lib.weaveffi_error_clear.restype = None -# Async completion callbacks receive a heap-boxed error the consumer owns; -# weaveffi_error_free releases the message, the payload, and the box. -_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] -_lib.weaveffi_error_free.restype = None -# The free helpers take raw addresses (`c_void_p`) so wrappers can release -# owned producer allocations they hold as plain integers or typed pointers. -_lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] -_lib.weaveffi_free_string.restype = None -_lib.weaveffi_free_bytes.argtypes = [ctypes.c_void_p, ctypes.c_size_t] -_lib.weaveffi_free_bytes.restype = None - - -def _check_error(err: _WeaveFFIErrorStruct) -> None: - if err.code != 0: - code = err.code - message = err.message.decode("utf-8") if err.message else "" - _lib.weaveffi_error_clear(ctypes.byref(err)) - raise WeaveFFIError(code, message) - - -class _PointerGuard(contextlib.AbstractContextManager): - def __init__(self, ptr, free_fn) -> None: - self.ptr = ptr - self._free_fn = free_fn - - def __exit__(self, *exc) -> bool: - if self.ptr is not None: - self._free_fn(self.ptr) - self.ptr = None - return False - - -def _string_to_bytes(s: Optional[str]) -> Optional[bytes]: - if s is None: - return None - return s.encode("utf-8") - - -def _bytes_to_string(ptr) -> Optional[str]: - if ptr is None: - return None - return ptr.decode("utf-8") - - -def _take_string(ptr) -> Optional[str]: - """Copy an owned C string (a raw address) and free the producer buffer.""" - if not ptr: - return None - _s = ctypes.string_at(ptr).decode("utf-8") - _lib.weaveffi_free_string(ptr) - return _s - - -class _BufferWriter: - """Encodes values into the WeaveFFI value-buffer wire format: - little-endian, packed, no alignment.""" - - def __init__(self) -> None: - self._buf = bytearray() - - def finish(self) -> bytes: - return bytes(self._buf) - - def write_bool(self, v) -> None: - self._buf.append(1 if v else 0) - - def write_i8(self, v: int) -> None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf.append(1 if present else 0) - - def write_string(self, v: str) -> None: - _b = v.encode("utf-8") - self.write_len(len(_b)) - self._buf += _b - - def write_bytes(self, v: bytes) -> None: - self.write_len(len(v)) - self._buf += bytes(v) - - -class _BufferReader: - """Decodes values from the WeaveFFI value-buffer wire format, rejecting - truncated buffers, invalid flag bytes, invalid UTF-8, and oversized - length prefixes.""" - - def __init__(self, data: bytes) -> None: - self._data = memoryview(data) - self._pos = 0 - - def _take(self, n: int, what: str) -> memoryview: - if len(self._data) - self._pos < n: - raise WeaveFFIError(-1, f"malformed value buffer: truncated {what}") - _view = self._data[self._pos:self._pos + n] - self._pos += n - return _view - - def read_bool(self) -> bool: - _b = self._take(1, "bool")[0] - if _b > 1: - raise WeaveFFIError(-1, "malformed value buffer: invalid bool byte") - return _b == 1 - - def read_i8(self) -> int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" float: - return struct.unpack(" float: - return struct.unpack(" int: - _n = struct.unpack(" len(self._data) - self._pos: - raise WeaveFFIError( - -1, "malformed value buffer: length prefix exceeds remaining bytes" - ) - return _n - - def read_option_flag(self) -> bool: - _b = self._take(1, "option flag")[0] - if _b > 1: - raise WeaveFFIError(-1, "malformed value buffer: invalid option flag") - return _b == 1 - - def read_string(self) -> str: - _n = self.read_len() - try: - return str(self._take(_n, "string data"), "utf-8") - except UnicodeDecodeError as _e: - raise WeaveFFIError( - -1, "malformed value buffer: string is not valid UTF-8" - ) from _e - - def read_bytes(self) -> bytes: - _n = self.read_len() - return bytes(self._take(_n, "bytes data")) - - def expect_end(self) -> None: - if self._pos != len(self._data): - raise WeaveFFIError(-1, "malformed value buffer: trailing bytes") - - -def _decode_buffer(data: bytes, read_fn): - """Decode exactly one value from `data` using `read_fn(reader)`.""" - _r = _BufferReader(data) - _v = read_fn(_r) - _r.expect_end() - return _v - - -def _take_buffer(ptr, length) -> bytes: - """Copy an owned value buffer (a raw address) and release it with - weaveffi_free_bytes.""" - if not ptr: - return b"" - _data = ctypes.string_at(ptr, length) if length else b"" - _lib.weaveffi_free_bytes(ptr, ctypes.c_size_t(length)) - return _data - - -# Registered listener trampolines, keyed by subscription id. Holding -# the ctypes function objects here keeps them alive until unregistered; -# without this the GC could collect a trampoline the producer still calls. -_listener_refs: Dict[int, object] = {} - - -# === Module: events === - -# Callback type OnMessage: Callable[[str], None] -_CFUNC_weaveffi_events_OnMessage_fn = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_void_p) - - -def register_message_listener(callback: Callable[[str], None]) -> int: - """Register a OnMessage listener. Returns a subscription id for unregister_message_listener().""" - def _trampoline(message, _context): - callback(_bytes_to_string(message)) - _cfunc = _CFUNC_weaveffi_events_OnMessage_fn(_trampoline) - _fn = _lib.weaveffi_events_register_message_listener - _fn.argtypes = [_CFUNC_weaveffi_events_OnMessage_fn, ctypes.c_void_p] - _fn.restype = ctypes.c_uint64 - _listener_id = int(_fn(_cfunc, None)) - _listener_refs[_listener_id] = _cfunc - return _listener_id - - -def unregister_message_listener(listener_id: int) -> None: - """Unregister a listener previously registered with register_message_listener().""" - _fn = _lib.weaveffi_events_unregister_message_listener - _fn.argtypes = [ctypes.c_uint64] - _fn.restype = None - _fn(ctypes.c_uint64(listener_id)) - _listener_refs.pop(listener_id, None) - - -def send_message(text: str) -> None: - """Send a message, triggering the OnMessage callback - """ - _fn = _lib.weaveffi_events_send_message - _fn.argtypes = [ctypes.c_char_p, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = None - _err = _WeaveFFIErrorStruct() - _fn(_string_to_bytes(text), ctypes.byref(_err)) - _check_error(_err) - - -class _GetMessagesIterator: - """Lazy iterator over a producer stream: each step pulls one element - across the C boundary. The native handle is released exactly once, on - exhaustion, on close(), or when the iterator is garbage collected.""" - - def __init__(self, ptr): - self._ptr = ptr - self._done = False - - def __iter__(self): - return self - - def __next__(self): - if self._done: - raise StopIteration - _next_fn = _lib.weaveffi_events_GetMessagesIterator_next - _next_fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_WeaveFFIErrorStruct)] - _next_fn.restype = ctypes.c_int32 - _out_item = ctypes.c_void_p() - _err = _WeaveFFIErrorStruct() - _has = _next_fn(self._ptr, ctypes.byref(_out_item), ctypes.byref(_err)) - _check_error(_err) - if not _has: - self._done = True - self._destroy() - raise StopIteration - return _take_string(_out_item.value) - - def close(self): - """Release the native iterator without draining it.""" - self._done = True - self._destroy() - - def _destroy(self): - if self._ptr is not None: - _destroy_fn = _lib.weaveffi_events_GetMessagesIterator_destroy - _destroy_fn.argtypes = [ctypes.c_void_p] - _destroy_fn.restype = None - _destroy_fn(self._ptr) - self._ptr = None - - def __del__(self): - self._destroy() - - -def get_messages() -> Iterator[str]: - """ - Return an iterator over all sent messages - - 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). - """ - _fn = _lib.weaveffi_events_get_messages - _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _result = _fn(ctypes.byref(_err)) - _check_error(_err) - return _GetMessagesIterator(_result) - -# END weaveffi.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_events__weaveffi_weaveffi_pyi.snap b/crates/weaveffi-cli/tests/snapshots/python_events__weaveffi_weaveffi_pyi.snap deleted file mode 100644 index b4013c67..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_events__weaveffi_weaveffi_pyi.snap +++ /dev/null @@ -1,25 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional, Type - -class WeaveFFIError(Exception): - code: int - message: str - def __init__(self, code: int, message: str) -> None: ... - -def register_message_listener(callback: Callable[[str], None]) -> int: ... -def unregister_message_listener(listener_id: int) -> None: ... - -# Send a message, triggering the OnMessage callback -def send_message(text: str) -> None: ... - -# Return an iterator over all sent messages -def get_messages() -> Iterator[str]: ... - -# END weaveffi.pyi diff --git a/crates/weaveffi-cli/tests/snapshots/python_inventory__README_md.snap b/crates/weaveffi-cli/tests/snapshots/python_inventory__README_md.snap deleted file mode 100644 index e3b8a7b9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_inventory__README_md.snap +++ /dev/null @@ -1,35 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# weaveffi (Python) - -Auto-generated Python bindings using ctypes. - -## Prerequisites - -- Python >= 3.8 -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path. - -## Install - -```bash -pip install . -``` - -## Development install - -```bash -pip install -e . -``` - -## Usage - -```python -from weaveffi import * -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/python_inventory__pyproject_toml.snap b/crates/weaveffi-cli/tests/snapshots/python_inventory__pyproject_toml.snap deleted file mode 100644 index c07695fa..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_inventory__pyproject_toml.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "weaveffi" -version = "0.1.0" -description = "weaveffi bindings generated by WeaveFFI" -requires-python = ">=3.8" - -[tool.setuptools] -packages = ["weaveffi"] - -# END pyproject.toml diff --git a/crates/weaveffi-cli/tests/snapshots/python_inventory__setup_py.snap b/crates/weaveffi-cli/tests/snapshots/python_inventory__setup_py.snap deleted file mode 100644 index 4c5da6f5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_inventory__setup_py.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -from setuptools import setup - -setup( - name="weaveffi", - version="0.1.0", - packages=["weaveffi"], -) - -# END setup.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_inventory__weaveffi___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_inventory__weaveffi___init___py.snap deleted file mode 100644 index e4ae4ecc..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_inventory__weaveffi___init___py.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -from .weaveffi import * # noqa: F401,F403 - -# END __init__.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_inventory__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_inventory__weaveffi_weaveffi_py.snap deleted file mode 100644 index dd68682d..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_inventory__weaveffi_weaveffi_py.snap +++ /dev/null @@ -1,657 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -"""WeaveFFI Python ctypes bindings (auto-generated)""" -import contextlib -import ctypes -import os -import platform -import struct -from dataclasses import dataclass -from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional - - -class WeaveFFIError(Exception): - def __init__(self, code: int, message: str) -> None: - self.code = code - self.message = message - super().__init__(f"({code}) {message}") - - -class _WeaveFFIErrorStruct(ctypes.Structure): - _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 in WEAVEFFI_LIBRARY wins, so callers can point at a - # specific build artifact regardless of its file name or location. - override = os.environ.get("WEAVEFFI_LIBRARY") - if override: - return ctypes.CDLL(override) - system = platform.system() - if system == "Darwin": - name = "libweaveffi.dylib" - elif system == "Windows": - name = "weaveffi.dll" - else: - name = "libweaveffi.so" - return ctypes.CDLL(name) - - -_lib = _load_library() -_lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] -_lib.weaveffi_error_clear.restype = None -# Async completion callbacks receive a heap-boxed error the consumer owns; -# weaveffi_error_free releases the message, the payload, and the box. -_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] -_lib.weaveffi_error_free.restype = None -# The free helpers take raw addresses (`c_void_p`) so wrappers can release -# owned producer allocations they hold as plain integers or typed pointers. -_lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] -_lib.weaveffi_free_string.restype = None -_lib.weaveffi_free_bytes.argtypes = [ctypes.c_void_p, ctypes.c_size_t] -_lib.weaveffi_free_bytes.restype = None - - -def _check_error(err: _WeaveFFIErrorStruct) -> None: - if err.code != 0: - code = err.code - message = err.message.decode("utf-8") if err.message else "" - _lib.weaveffi_error_clear(ctypes.byref(err)) - raise WeaveFFIError(code, message) - - -class _PointerGuard(contextlib.AbstractContextManager): - def __init__(self, ptr, free_fn) -> None: - self.ptr = ptr - self._free_fn = free_fn - - def __exit__(self, *exc) -> bool: - if self.ptr is not None: - self._free_fn(self.ptr) - self.ptr = None - return False - - -def _string_to_bytes(s: Optional[str]) -> Optional[bytes]: - if s is None: - return None - return s.encode("utf-8") - - -def _bytes_to_string(ptr) -> Optional[str]: - if ptr is None: - return None - return ptr.decode("utf-8") - - -def _take_string(ptr) -> Optional[str]: - """Copy an owned C string (a raw address) and free the producer buffer.""" - if not ptr: - return None - _s = ctypes.string_at(ptr).decode("utf-8") - _lib.weaveffi_free_string(ptr) - return _s - - -class _BufferWriter: - """Encodes values into the WeaveFFI value-buffer wire format: - little-endian, packed, no alignment.""" - - def __init__(self) -> None: - self._buf = bytearray() - - def finish(self) -> bytes: - return bytes(self._buf) - - def write_bool(self, v) -> None: - self._buf.append(1 if v else 0) - - def write_i8(self, v: int) -> None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf.append(1 if present else 0) - - def write_string(self, v: str) -> None: - _b = v.encode("utf-8") - self.write_len(len(_b)) - self._buf += _b - - def write_bytes(self, v: bytes) -> None: - self.write_len(len(v)) - self._buf += bytes(v) - - -class _BufferReader: - """Decodes values from the WeaveFFI value-buffer wire format, rejecting - truncated buffers, invalid flag bytes, invalid UTF-8, and oversized - length prefixes.""" - - def __init__(self, data: bytes) -> None: - self._data = memoryview(data) - self._pos = 0 - - def _take(self, n: int, what: str) -> memoryview: - if len(self._data) - self._pos < n: - raise WeaveFFIError(-1, f"malformed value buffer: truncated {what}") - _view = self._data[self._pos:self._pos + n] - self._pos += n - return _view - - def read_bool(self) -> bool: - _b = self._take(1, "bool")[0] - if _b > 1: - raise WeaveFFIError(-1, "malformed value buffer: invalid bool byte") - return _b == 1 - - def read_i8(self) -> int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" float: - return struct.unpack(" float: - return struct.unpack(" int: - _n = struct.unpack(" len(self._data) - self._pos: - raise WeaveFFIError( - -1, "malformed value buffer: length prefix exceeds remaining bytes" - ) - return _n - - def read_option_flag(self) -> bool: - _b = self._take(1, "option flag")[0] - if _b > 1: - raise WeaveFFIError(-1, "malformed value buffer: invalid option flag") - return _b == 1 - - def read_string(self) -> str: - _n = self.read_len() - try: - return str(self._take(_n, "string data"), "utf-8") - except UnicodeDecodeError as _e: - raise WeaveFFIError( - -1, "malformed value buffer: string is not valid UTF-8" - ) from _e - - def read_bytes(self) -> bytes: - _n = self.read_len() - return bytes(self._take(_n, "bytes data")) - - def expect_end(self) -> None: - if self._pos != len(self._data): - raise WeaveFFIError(-1, "malformed value buffer: trailing bytes") - - -def _decode_buffer(data: bytes, read_fn): - """Decode exactly one value from `data` using `read_fn(reader)`.""" - _r = _BufferReader(data) - _v = read_fn(_r) - _r.expect_end() - return _v - - -def _take_buffer(ptr, length) -> bytes: - """Copy an owned value buffer (a raw address) and release it with - weaveffi_free_bytes.""" - if not ptr: - return b"" - _data = ctypes.string_at(ptr, length) if length else b"" - _lib.weaveffi_free_bytes(ptr, ctypes.c_size_t(length)) - return _data - - -# === Module: products === - -class ProductsError(WeaveFFIError): - """Base exception for the `products` module's error domain.""" - - -class InvalidPrice(ProductsError): - """price must be positive""" - - CODE = 1 - - def __init__(self, message: str = "price must be positive") -> None: - super().__init__(1, message) - - -class ProductNotFound(ProductsError): - """product not found""" - - CODE = 2 - - def __init__(self, message: str = "product not found") -> None: - super().__init__(2, message) - - -ProductsError.InvalidPrice = InvalidPrice -ProductsError.ProductNotFound = ProductNotFound - - -_PRODUCTS_ERROR_CODES: Dict[int, type] = { - 1: InvalidPrice, - 2: ProductNotFound, -} - - -def _products_error_from(code: int, message: str, payload: bytes = b"") -> WeaveFFIError: - """Build the ProductsError subclass matching `code`, or a generic - WeaveFFIError for codes outside the domain (panics, marshalling).""" - _cls = _PRODUCTS_ERROR_CODES.get(code) - if _cls is None: - return WeaveFFIError(code, message) - _exc = _cls(message) if message else _cls() - return _exc - - -def _check_products_error(err: _WeaveFFIErrorStruct) -> None: - if err.code != 0: - code = err.code - message = err.message.decode("utf-8") if err.message else "" - payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" - _lib.weaveffi_error_clear(ctypes.byref(err)) - raise _products_error_from(code, message, payload) - - -class Category(IntEnum): - Electronics = 0 - Clothing = 1 - Food = 2 - Books = 3 - - -@dataclass -class Product: - id: int - name: str - description: Optional[str] - price: float - category: "Category" - tags: List[str] - - -def _write_Product(_w: _BufferWriter, value: "Product") -> None: - _w.write_i64(value.id) - _w.write_string(value.name) - if value.description is None: - _w.write_option_flag(False) - else: - _w.write_option_flag(True) - _w.write_string(value.description) - _w.write_f64(value.price) - _w.write_i32(value.category) - _w.write_len(len(value.tags)) - for _e0 in value.tags: - _w.write_string(_e0) - - -def _read_Product(_r: _BufferReader) -> "Product": - return Product( - id=_r.read_i64(), - name=_r.read_string(), - description=(_r.read_string() if _r.read_option_flag() else None), - price=_r.read_f64(), - category=Category(_r.read_i32()), - tags=[_r.read_string() for _i0 in range(_r.read_len())], - ) - - -def _pack_Product(value: "Product") -> bytes: - _w = _BufferWriter() - _write_Product(_w, value) - return _w.finish() - - -def _unpack_Product(data: bytes) -> "Product": - return _decode_buffer(data, _read_Product) - - -class Catalog: - """A product catalog owning its products""" - - @classmethod - def _from_ptr(cls, ptr) -> "Catalog": - _obj = cls.__new__(cls) - _obj._ptr = ptr - return _obj - - def __init__(self) -> None: - self._ptr = None - _fn = _lib.weaveffi_products_Catalog_new - _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _result = _fn(ctypes.byref(_err)) - _check_error(_err) - if _result is None: - raise WeaveFFIError(-1, "null pointer") - self._ptr = _result - - - def __del__(self) -> None: - if self._ptr is not None: - _lib.weaveffi_products_Catalog_destroy.argtypes = [ctypes.c_void_p] - _lib.weaveffi_products_Catalog_destroy.restype = None - _lib.weaveffi_products_Catalog_destroy(self._ptr) - self._ptr = None - - def add_product(self, name: str, price: float, category: "Category") -> "Product": - """ - Raises - ------ - ProductsError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_products_Catalog_add_product - _fn.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_double, ctypes.c_int32, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _out_len = ctypes.c_size_t(0) - _result = _fn(self._ptr, _string_to_bytes(name), price, category.value, ctypes.byref(_out_len), ctypes.byref(_err)) - _check_products_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_Product(_data) - - - def get_product(self, id: int) -> "Product": - """ - Raises - ------ - ProductsError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_products_Catalog_get_product - _fn.argtypes = [ctypes.c_void_p, ctypes.c_int64, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _out_len = ctypes.c_size_t(0) - _result = _fn(self._ptr, id, ctypes.byref(_out_len), ctypes.byref(_err)) - _check_products_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_Product(_data) - - - def search(self, category: "Category") -> List["Product"]: - _fn = _lib.weaveffi_products_Catalog_search - _fn.argtypes = [ctypes.c_void_p, ctypes.c_int32, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _out_len = ctypes.c_size_t(0) - _result = _fn(self._ptr, category.value, ctypes.byref(_out_len), ctypes.byref(_err)) - _check_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _decode_buffer(_data, lambda _r: [_read_Product(_r) for _i0 in range(_r.read_len())]) - - - def update_price(self, id: int, price: float) -> bool: - """ - Raises - ------ - ProductsError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_products_Catalog_update_price - _fn.argtypes = [ctypes.c_void_p, ctypes.c_int64, ctypes.c_double, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(self._ptr, id, price, ctypes.byref(_err)) - _check_products_error(_err) - return bool(_result) - - - def remove(self, id: int) -> bool: - _fn = _lib.weaveffi_products_Catalog_remove - _fn.argtypes = [ctypes.c_void_p, ctypes.c_int64, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(self._ptr, id, ctypes.byref(_err)) - _check_error(_err) - return bool(_result) - - - -# === Module: orders === - -class OrdersError(WeaveFFIError): - """Base exception for the `orders` module's error domain.""" - - -class OrderNotFound(OrdersError): - """order not found""" - - CODE = 1 - - def __init__(self, message: str = "order not found") -> None: - super().__init__(1, message) - - -class EmptyOrder(OrdersError): - """order must contain at least one item""" - - CODE = 2 - - def __init__(self, message: str = "order must contain at least one item") -> None: - super().__init__(2, message) - - -OrdersError.OrderNotFound = OrderNotFound -OrdersError.EmptyOrder = EmptyOrder - - -_ORDERS_ERROR_CODES: Dict[int, type] = { - 1: OrderNotFound, - 2: EmptyOrder, -} - - -def _orders_error_from(code: int, message: str, payload: bytes = b"") -> WeaveFFIError: - """Build the OrdersError subclass matching `code`, or a generic - WeaveFFIError for codes outside the domain (panics, marshalling).""" - _cls = _ORDERS_ERROR_CODES.get(code) - if _cls is None: - return WeaveFFIError(code, message) - _exc = _cls(message) if message else _cls() - return _exc - - -def _check_orders_error(err: _WeaveFFIErrorStruct) -> None: - if err.code != 0: - code = err.code - message = err.message.decode("utf-8") if err.message else "" - payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" - _lib.weaveffi_error_clear(ctypes.byref(err)) - raise _orders_error_from(code, message, payload) - - -@dataclass -class OrderItem: - product_id: int - quantity: int - unit_price: float - - -def _write_OrderItem(_w: _BufferWriter, value: "OrderItem") -> None: - _w.write_i64(value.product_id) - _w.write_i32(value.quantity) - _w.write_f64(value.unit_price) - - -def _read_OrderItem(_r: _BufferReader) -> "OrderItem": - return OrderItem( - product_id=_r.read_i64(), - quantity=_r.read_i32(), - unit_price=_r.read_f64(), - ) - - -def _pack_OrderItem(value: "OrderItem") -> bytes: - _w = _BufferWriter() - _write_OrderItem(_w, value) - return _w.finish() - - -def _unpack_OrderItem(data: bytes) -> "OrderItem": - return _decode_buffer(data, _read_OrderItem) - - -@dataclass -class Order: - id: int - items: List["OrderItem"] - total: float - status: str - - -def _write_Order(_w: _BufferWriter, value: "Order") -> None: - _w.write_i64(value.id) - _w.write_len(len(value.items)) - for _e0 in value.items: - _write_OrderItem(_w, _e0) - _w.write_f64(value.total) - _w.write_string(value.status) - - -def _read_Order(_r: _BufferReader) -> "Order": - return Order( - id=_r.read_i64(), - items=[_read_OrderItem(_r) for _i0 in range(_r.read_len())], - total=_r.read_f64(), - status=_r.read_string(), - ) - - -def _pack_Order(value: "Order") -> bytes: - _w = _BufferWriter() - _write_Order(_w, value) - return _w.finish() - - -def _unpack_Order(data: bytes) -> "Order": - return _decode_buffer(data, _read_Order) - - -def create_order(items: List["OrderItem"]) -> int: - """ - Raises - ------ - OrdersError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_orders_create_order - _fn.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int64 - _items_w = _BufferWriter() - _items_w.write_len(len(items)) - for _e0 in items: - _write_OrderItem(_items_w, _e0) - _items_buf = _items_w.finish() - _err = _WeaveFFIErrorStruct() - _result = _fn(_items_buf, len(_items_buf), ctypes.byref(_err)) - _check_orders_error(_err) - return _result - - -def get_order(id: int) -> "Order": - """ - Raises - ------ - OrdersError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_orders_get_order - _fn.argtypes = [ctypes.c_int64, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _out_len = ctypes.c_size_t(0) - _result = _fn(id, ctypes.byref(_out_len), ctypes.byref(_err)) - _check_orders_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_Order(_data) - - -def cancel_order(id: int) -> bool: - _fn = _lib.weaveffi_orders_cancel_order - _fn.argtypes = [ctypes.c_int64, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(id, ctypes.byref(_err)) - _check_error(_err) - return bool(_result) - - -def add_product_to_order(order_id: int, product: "Product") -> bool: - _fn = _lib.weaveffi_orders_add_product_to_order - _fn.argtypes = [ctypes.c_int64, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _product_buf = _pack_Product(product) - _err = _WeaveFFIErrorStruct() - _result = _fn(order_id, _product_buf, len(_product_buf), ctypes.byref(_err)) - _check_error(_err) - return bool(_result) - -# END weaveffi.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_inventory__weaveffi_weaveffi_pyi.snap b/crates/weaveffi-cli/tests/snapshots/python_inventory__weaveffi_weaveffi_pyi.snap deleted file mode 100644 index 4efc0d39..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_inventory__weaveffi_weaveffi_pyi.snap +++ /dev/null @@ -1,87 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional, Type - -class WeaveFFIError(Exception): - code: int - message: str - def __init__(self, code: int, message: str) -> None: ... - -class ProductsError(WeaveFFIError): - InvalidPrice: Type["InvalidPrice"] - ProductNotFound: Type["ProductNotFound"] - def __init__(self, code: int, message: str) -> None: ... - -class InvalidPrice(ProductsError): - CODE: int - def __init__(self, message: str = ...) -> None: ... - -class ProductNotFound(ProductsError): - CODE: int - def __init__(self, message: str = ...) -> None: ... - -class Category(IntEnum): - Electronics: int - Clothing: int - Food: int - Books: int - -class Product: - id: int - name: str - description: Optional[str] - price: float - category: "Category" - tags: List[str] - def __init__(self, id: int, name: str, description: Optional[str], price: float, category: "Category", tags: List[str]) -> None: ... - -# A product catalog owning its products -class Catalog: - def __init__(self) -> None: ... - def add_product(self, name: str, price: float, category: "Category") -> "Product": ... - def get_product(self, id: int) -> "Product": ... - def search(self, category: "Category") -> List["Product"]: ... - def update_price(self, id: int, price: float) -> bool: ... - def remove(self, id: int) -> bool: ... - -class OrdersError(WeaveFFIError): - OrderNotFound: Type["OrderNotFound"] - EmptyOrder: Type["EmptyOrder"] - def __init__(self, code: int, message: str) -> None: ... - -class OrderNotFound(OrdersError): - CODE: int - def __init__(self, message: str = ...) -> None: ... - -class EmptyOrder(OrdersError): - CODE: int - def __init__(self, message: str = ...) -> None: ... - -class OrderItem: - product_id: int - quantity: int - unit_price: float - def __init__(self, product_id: int, quantity: int, unit_price: float) -> None: ... - -class Order: - id: int - items: List["OrderItem"] - total: float - status: str - def __init__(self, id: int, items: List["OrderItem"], total: float, status: str) -> None: ... - -def create_order(items: List["OrderItem"]) -> int: ... - -def get_order(id: int) -> "Order": ... - -def cancel_order(id: int) -> bool: ... - -def add_product_to_order(order_id: int, product: "Product") -> bool: ... - -# END weaveffi.pyi diff --git a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__weaveffi_weaveffi_py.snap index 6fef1c15..4c137ce8 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__weaveffi_weaveffi_py.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_kitchen_sink__weaveffi_weaveffi_py.snap @@ -49,6 +49,32 @@ def _load_library() -> ctypes.CDLL: _lib = _load_library() + +# The ABI revision these bindings were generated against. Checked before any +# other symbol is bound so a mismatched producer fails at import time instead +# of misreading the error struct or a value buffer later. +_ABI_VERSION = 1 + + +def _check_abi_version(lib: ctypes.CDLL) -> None: + try: + fn = lib.weaveffi_abi_version + except AttributeError: + raise ImportError( + "the loaded WeaveFFI library predates ABI versioning " + f"(these bindings expect ABI revision {_ABI_VERSION})" + ) from None + fn.argtypes = [] + fn.restype = ctypes.c_uint32 + found = fn() + if found != _ABI_VERSION: + raise ImportError( + f"WeaveFFI ABI mismatch: these bindings expect revision {_ABI_VERSION} " + f"but the loaded library reports revision {found}" + ) + + +_check_abi_version(_lib) _lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] _lib.weaveffi_error_clear.restype = None # Async completion callbacks receive a heap-boxed error the consumer owns; diff --git a/crates/weaveffi-cli/tests/snapshots/python_kvstore__README_md.snap b/crates/weaveffi-cli/tests/snapshots/python_kvstore__README_md.snap deleted file mode 100644 index e3b8a7b9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_kvstore__README_md.snap +++ /dev/null @@ -1,35 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# weaveffi (Python) - -Auto-generated Python bindings using ctypes. - -## Prerequisites - -- Python >= 3.8 -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path. - -## Install - -```bash -pip install . -``` - -## Development install - -```bash -pip install -e . -``` - -## Usage - -```python -from weaveffi import * -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/python_kvstore__pyproject_toml.snap b/crates/weaveffi-cli/tests/snapshots/python_kvstore__pyproject_toml.snap deleted file mode 100644 index c07695fa..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_kvstore__pyproject_toml.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "weaveffi" -version = "0.1.0" -description = "weaveffi bindings generated by WeaveFFI" -requires-python = ">=3.8" - -[tool.setuptools] -packages = ["weaveffi"] - -# END pyproject.toml diff --git a/crates/weaveffi-cli/tests/snapshots/python_kvstore__setup_py.snap b/crates/weaveffi-cli/tests/snapshots/python_kvstore__setup_py.snap deleted file mode 100644 index 4c5da6f5..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_kvstore__setup_py.snap +++ /dev/null @@ -1,16 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -from setuptools import setup - -setup( - name="weaveffi", - version="0.1.0", - packages=["weaveffi"], -) - -# END setup.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_kvstore__weaveffi___init___py.snap b/crates/weaveffi-cli/tests/snapshots/python_kvstore__weaveffi___init___py.snap deleted file mode 100644 index e4ae4ecc..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_kvstore__weaveffi___init___py.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -from .weaveffi import * # noqa: F401,F403 - -# END __init__.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_kvstore__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_kvstore__weaveffi_weaveffi_py.snap deleted file mode 100644 index 2565cc71..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_kvstore__weaveffi_weaveffi_py.snap +++ /dev/null @@ -1,807 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -"""WeaveFFI Python ctypes bindings (auto-generated)""" -import contextlib -import ctypes -import os -import platform -import struct -from dataclasses import dataclass -from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional - - -class WeaveFFIError(Exception): - def __init__(self, code: int, message: str) -> None: - self.code = code - self.message = message - super().__init__(f"({code}) {message}") - - -class _WeaveFFIErrorStruct(ctypes.Structure): - _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 in WEAVEFFI_LIBRARY wins, so callers can point at a - # specific build artifact regardless of its file name or location. - override = os.environ.get("WEAVEFFI_LIBRARY") - if override: - return ctypes.CDLL(override) - system = platform.system() - if system == "Darwin": - name = "libweaveffi.dylib" - elif system == "Windows": - name = "weaveffi.dll" - else: - name = "libweaveffi.so" - return ctypes.CDLL(name) - - -_lib = _load_library() -_lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] -_lib.weaveffi_error_clear.restype = None -# Async completion callbacks receive a heap-boxed error the consumer owns; -# weaveffi_error_free releases the message, the payload, and the box. -_lib.weaveffi_error_free.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] -_lib.weaveffi_error_free.restype = None -# The free helpers take raw addresses (`c_void_p`) so wrappers can release -# owned producer allocations they hold as plain integers or typed pointers. -_lib.weaveffi_free_string.argtypes = [ctypes.c_void_p] -_lib.weaveffi_free_string.restype = None -_lib.weaveffi_free_bytes.argtypes = [ctypes.c_void_p, ctypes.c_size_t] -_lib.weaveffi_free_bytes.restype = None - - -def _check_error(err: _WeaveFFIErrorStruct) -> None: - if err.code != 0: - code = err.code - message = err.message.decode("utf-8") if err.message else "" - _lib.weaveffi_error_clear(ctypes.byref(err)) - raise WeaveFFIError(code, message) - - -class _PointerGuard(contextlib.AbstractContextManager): - def __init__(self, ptr, free_fn) -> None: - self.ptr = ptr - self._free_fn = free_fn - - def __exit__(self, *exc) -> bool: - if self.ptr is not None: - self._free_fn(self.ptr) - self.ptr = None - return False - - -def _string_to_bytes(s: Optional[str]) -> Optional[bytes]: - if s is None: - return None - return s.encode("utf-8") - - -def _bytes_to_string(ptr) -> Optional[str]: - if ptr is None: - return None - return ptr.decode("utf-8") - - -def _take_string(ptr) -> Optional[str]: - """Copy an owned C string (a raw address) and free the producer buffer.""" - if not ptr: - return None - _s = ctypes.string_at(ptr).decode("utf-8") - _lib.weaveffi_free_string(ptr) - return _s - - -class _BufferWriter: - """Encodes values into the WeaveFFI value-buffer wire format: - little-endian, packed, no alignment.""" - - def __init__(self) -> None: - self._buf = bytearray() - - def finish(self) -> bytes: - return bytes(self._buf) - - def write_bool(self, v) -> None: - self._buf.append(1 if v else 0) - - def write_i8(self, v: int) -> None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf += struct.pack(" None: - self._buf.append(1 if present else 0) - - def write_string(self, v: str) -> None: - _b = v.encode("utf-8") - self.write_len(len(_b)) - self._buf += _b - - def write_bytes(self, v: bytes) -> None: - self.write_len(len(v)) - self._buf += bytes(v) - - -class _BufferReader: - """Decodes values from the WeaveFFI value-buffer wire format, rejecting - truncated buffers, invalid flag bytes, invalid UTF-8, and oversized - length prefixes.""" - - def __init__(self, data: bytes) -> None: - self._data = memoryview(data) - self._pos = 0 - - def _take(self, n: int, what: str) -> memoryview: - if len(self._data) - self._pos < n: - raise WeaveFFIError(-1, f"malformed value buffer: truncated {what}") - _view = self._data[self._pos:self._pos + n] - self._pos += n - return _view - - def read_bool(self) -> bool: - _b = self._take(1, "bool")[0] - if _b > 1: - raise WeaveFFIError(-1, "malformed value buffer: invalid bool byte") - return _b == 1 - - def read_i8(self) -> int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" int: - return struct.unpack(" float: - return struct.unpack(" float: - return struct.unpack(" int: - _n = struct.unpack(" len(self._data) - self._pos: - raise WeaveFFIError( - -1, "malformed value buffer: length prefix exceeds remaining bytes" - ) - return _n - - def read_option_flag(self) -> bool: - _b = self._take(1, "option flag")[0] - if _b > 1: - raise WeaveFFIError(-1, "malformed value buffer: invalid option flag") - return _b == 1 - - def read_string(self) -> str: - _n = self.read_len() - try: - return str(self._take(_n, "string data"), "utf-8") - except UnicodeDecodeError as _e: - raise WeaveFFIError( - -1, "malformed value buffer: string is not valid UTF-8" - ) from _e - - def read_bytes(self) -> bytes: - _n = self.read_len() - return bytes(self._take(_n, "bytes data")) - - def expect_end(self) -> None: - if self._pos != len(self._data): - raise WeaveFFIError(-1, "malformed value buffer: trailing bytes") - - -def _decode_buffer(data: bytes, read_fn): - """Decode exactly one value from `data` using `read_fn(reader)`.""" - _r = _BufferReader(data) - _v = read_fn(_r) - _r.expect_end() - return _v - - -def _take_buffer(ptr, length) -> bytes: - """Copy an owned value buffer (a raw address) and release it with - weaveffi_free_bytes.""" - if not ptr: - return b"" - _data = ctypes.string_at(ptr, length) if length else b"" - _lib.weaveffi_free_bytes(ptr, ctypes.c_size_t(length)) - return _data - -import asyncio -import threading - - -# Pending async completion trampolines, keyed by an integer token. -# Holding the ctypes function objects here keeps them alive until the -# producer fires the completion callback, even when the awaiting -# coroutine has been cancelled; each entry is removed on completion. -_async_pending: Dict[int, object] = {} -_async_lock = threading.Lock() -_async_next_token = 0 - - -def _async_register(cb) -> int: - global _async_next_token - with _async_lock: - _async_next_token += 1 - _token = _async_next_token - _async_pending[_token] = cb - return _token - - -# Registered listener trampolines, keyed by subscription id. Holding -# the ctypes function objects here keeps them alive until unregistered; -# without this the GC could collect a trampoline the producer still calls. -_listener_refs: Dict[int, object] = {} - - -# === Module: kv === - -class KvError(WeaveFFIError): - """Base exception for the `kv` module's error domain.""" - - -class KeyNotFound(KvError): - """No entry exists for the requested key""" - - CODE = 1001 - - def __init__(self, message: str = "key not found") -> None: - super().__init__(1001, message) - - -class Expired(KvError): - """The entry exists but its TTL has elapsed""" - - CODE = 1002 - - def __init__(self, message: str = "entry expired") -> None: - super().__init__(1002, message) - - -class StoreFull(KvError): - """The store has reached its capacity limit""" - - CODE = 1003 - - def __init__(self, message: str = "store has reached capacity") -> None: - super().__init__(1003, message) - - -class IoError(KvError): - """Underlying storage reported an I/O failure""" - - CODE = 1004 - - def __init__(self, message: str = "I/O failure") -> None: - super().__init__(1004, message) - - -KvError.KeyNotFound = KeyNotFound -KvError.Expired = Expired -KvError.StoreFull = StoreFull -KvError.IoError = IoError - - -_KV_ERROR_CODES: Dict[int, type] = { - 1001: KeyNotFound, - 1002: Expired, - 1003: StoreFull, - 1004: IoError, -} - - -def _kv_error_from(code: int, message: str, payload: bytes = b"") -> WeaveFFIError: - """Build the KvError subclass matching `code`, or a generic - WeaveFFIError for codes outside the domain (panics, marshalling).""" - _cls = _KV_ERROR_CODES.get(code) - if _cls is None: - return WeaveFFIError(code, message) - _exc = _cls(message) if message else _cls() - return _exc - - -def _check_kv_error(err: _WeaveFFIErrorStruct) -> None: - if err.code != 0: - code = err.code - message = err.message.decode("utf-8") if err.message else "" - payload = ctypes.string_at(err.payload_ptr, err.payload_len) if err.payload_ptr else b"" - _lib.weaveffi_error_clear(ctypes.byref(err)) - raise _kv_error_from(code, message, payload) - - -class EntryKind(IntEnum): - """Persistence semantics applied to a stored entry""" - # In-memory only; lost on close - Volatile = 0 - # Flushed to durable storage - Persistent = 1 - # Persistent and encrypted at rest - Encrypted = 2 - - -@dataclass -class Entry: - """A single key-value entry persisted in the store""" - - # Stable monotonic identifier assigned on insert - id: int - # UTF-8 lookup key - key: str - # Opaque binary payload - value: bytes - # Unix-timestamp seconds when the entry was created - created_at: int - # Optional unix-timestamp seconds at which the entry expires - expires_at: Optional[int] - # Free-form labels attached to the entry - tags: List[str] - # Arbitrary string-valued metadata pairs - metadata: Dict[str, str] - - -def _write_Entry(_w: _BufferWriter, value: "Entry") -> None: - _w.write_i64(value.id) - _w.write_string(value.key) - _w.write_bytes(value.value) - _w.write_i64(value.created_at) - if value.expires_at is None: - _w.write_option_flag(False) - else: - _w.write_option_flag(True) - _w.write_i64(value.expires_at) - _w.write_len(len(value.tags)) - for _e0 in value.tags: - _w.write_string(_e0) - _w.write_len(len(value.metadata)) - for _k0, _v0 in value.metadata.items(): - _w.write_string(_k0) - _w.write_string(_v0) - - -def _read_Entry(_r: _BufferReader) -> "Entry": - return Entry( - id=_r.read_i64(), - key=_r.read_string(), - value=_r.read_bytes(), - created_at=_r.read_i64(), - expires_at=(_r.read_i64() if _r.read_option_flag() else None), - tags=[_r.read_string() for _i0 in range(_r.read_len())], - metadata=dict((_r.read_string(), _r.read_string()) for _i0 in range(_r.read_len())), - ) - - -def _pack_Entry(value: "Entry") -> bytes: - _w = _BufferWriter() - _write_Entry(_w, value) - return _w.finish() - - -def _unpack_Entry(data: bytes) -> "Entry": - return _decode_buffer(data, _read_Entry) - - -class _StoreListKeysIterator: - """Lazy iterator over a producer stream: each step pulls one element - across the C boundary. The native handle is released exactly once, on - exhaustion, on close(), or when the iterator is garbage collected.""" - - def __init__(self, ptr): - self._ptr = ptr - self._done = False - - def __iter__(self): - return self - - def __next__(self): - if self._done: - raise StopIteration - _next_fn = _lib.weaveffi_kv_Store_ListKeysIterator_next - _next_fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(_WeaveFFIErrorStruct)] - _next_fn.restype = ctypes.c_int32 - _out_item = ctypes.c_void_p() - _err = _WeaveFFIErrorStruct() - _has = _next_fn(self._ptr, ctypes.byref(_out_item), ctypes.byref(_err)) - _check_kv_error(_err) - if not _has: - self._done = True - self._destroy() - raise StopIteration - return _take_string(_out_item.value) - - def close(self): - """Release the native iterator without draining it.""" - self._done = True - self._destroy() - - def _destroy(self): - if self._ptr is not None: - _destroy_fn = _lib.weaveffi_kv_Store_ListKeysIterator_destroy - _destroy_fn.argtypes = [ctypes.c_void_p] - _destroy_fn.restype = None - _destroy_fn(self._ptr) - self._ptr = None - - def __del__(self): - self._destroy() - - -class Store: - """An in-memory key-value store owning its entries; destroying the object releases all of its state""" - - @classmethod - def _from_ptr(cls, ptr) -> "Store": - _obj = cls.__new__(cls) - _obj._ptr = ptr - return _obj - - def __init__(self, _ptr: int) -> None: - self._ptr = _ptr - - - def __del__(self) -> None: - if self._ptr is not None: - _lib.weaveffi_kv_Store_destroy.argtypes = [ctypes.c_void_p] - _lib.weaveffi_kv_Store_destroy.restype = None - _lib.weaveffi_kv_Store_destroy(self._ptr) - self._ptr = None - - @classmethod - def open(cls, path: str) -> "Store": - """Open (or create) a store backed by the given filesystem path - - Raises - ------ - KvError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_kv_Store_open - _fn.argtypes = [ctypes.c_char_p, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _result = _fn(_string_to_bytes(path), ctypes.byref(_err)) - _check_kv_error(_err) - if _result is None: - raise WeaveFFIError(-1, "null pointer") - return cls._from_ptr(_result) - - - def put(self, key: str, value: bytes, kind: "EntryKind", ttl_seconds: Optional[int]) -> bool: - """Insert or replace a value, returning true on success - - Raises - ------ - KvError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_kv_Store_put - _fn.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, ctypes.c_int32, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _value_arr = (ctypes.c_uint8 * len(value))(*value) - _ttl_seconds_w = _BufferWriter() - if ttl_seconds is None: - _ttl_seconds_w.write_option_flag(False) - else: - _ttl_seconds_w.write_option_flag(True) - _ttl_seconds_w.write_i64(ttl_seconds) - _ttl_seconds_buf = _ttl_seconds_w.finish() - _err = _WeaveFFIErrorStruct() - _result = _fn(self._ptr, _string_to_bytes(key), _value_arr, len(value), kind.value, _ttl_seconds_buf, len(_ttl_seconds_buf), ctypes.byref(_err)) - _check_kv_error(_err) - return bool(_result) - - - def get(self, key: str) -> Optional["Entry"]: - """Look up an entry by key; returns null if missing or expired - - Raises - ------ - KvError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_kv_Store_get - _fn.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _out_len = ctypes.c_size_t(0) - _result = _fn(self._ptr, _string_to_bytes(key), ctypes.byref(_out_len), ctypes.byref(_err)) - _check_kv_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _decode_buffer(_data, lambda _r: (_read_Entry(_r) if _r.read_option_flag() else None)) - - - def delete(self, key: str) -> bool: - """Remove the entry for the given key, returning true if it existed - - Raises - ------ - KvError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_kv_Store_delete - _fn.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _err = _WeaveFFIErrorStruct() - _result = _fn(self._ptr, _string_to_bytes(key), ctypes.byref(_err)) - _check_kv_error(_err) - return bool(_result) - - - def list_keys(self, prefix: Optional[str]) -> Iterator[str]: - """ - Stream every key, optionally filtered by a prefix - - 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). - - Raises - ------ - KvError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_kv_Store_list_keys - _fn.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _prefix_w = _BufferWriter() - if prefix is None: - _prefix_w.write_option_flag(False) - else: - _prefix_w.write_option_flag(True) - _prefix_w.write_string(prefix) - _prefix_buf = _prefix_w.finish() - _err = _WeaveFFIErrorStruct() - _result = _fn(self._ptr, _prefix_buf, len(_prefix_buf), ctypes.byref(_err)) - _check_kv_error(_err) - return _StoreListKeysIterator(_result) - - - def count(self) -> int: - """Return the number of live entries in the store - """ - _fn = _lib.weaveffi_kv_Store_count - _fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int64 - _err = _WeaveFFIErrorStruct() - _result = _fn(self._ptr, ctypes.byref(_err)) - _check_error(_err) - return _result - - - def clear(self) -> None: - """Drop every entry from the store - """ - _fn = _lib.weaveffi_kv_Store_clear - _fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = None - _err = _WeaveFFIErrorStruct() - _fn(self._ptr, ctypes.byref(_err)) - _check_error(_err) - - - async def compact(self) -> int: - """Reclaim space asynchronously; returns the number of bytes reclaimed - - Raises - ------ - KvError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_kv_Store_compact_async - _loop = asyncio.get_running_loop() - _fut = _loop.create_future() - - def _cb_impl(context, err, result): - # Fires exactly once, on a producer thread: take ownership of - # the result here, then hop back to the event loop. - _state = {"err": None, "val": None} - if err and err.contents.code != 0: - _code = err.contents.code - _msg = err.contents.message.decode("utf-8") if err.contents.message else "" - _payload = ctypes.string_at(err.contents.payload_ptr, err.contents.payload_len) if err.contents.payload_ptr else b"" - _lib.weaveffi_error_free(err) - _state["err"] = _kv_error_from(_code, _msg, _payload) - else: - try: - _state["val"] = result - except Exception as _exc: - _state["err"] = _exc - - def _resolve(): - _async_pending.pop(_token, None) - # A cancelled future must not be resolved. - if _fut.cancelled(): - return - if _state["err"] is not None: - _fut.set_exception(_state["err"]) - else: - _fut.set_result(_state["val"]) - - _loop.call_soon_threadsafe(_resolve) - - _cb_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(_WeaveFFIErrorStruct), ctypes.c_int64) - _cb = _cb_type(_cb_impl) - _token = _async_register(_cb) # pinned until completion - _fn.argtypes = [ctypes.c_void_p, ctypes.c_void_p, _cb_type, ctypes.c_void_p] - _fn.restype = None - _fn(self._ptr, None, _cb, None) - return await _fut - - - def legacy_put(self, key: str, value: bytes) -> bool: - """Legacy single-shot put kept for compatibility - - Raises - ------ - KvError - If the call reports one of the domain's error codes. - """ - import warnings - warnings.warn("use put() with explicit kind", DeprecationWarning, stacklevel=2) - _fn = _lib.weaveffi_kv_Store_legacy_put - _fn.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int32 - _value_arr = (ctypes.c_uint8 * len(value))(*value) - _err = _WeaveFFIErrorStruct() - _result = _fn(self._ptr, _string_to_bytes(key), _value_arr, len(value), ctypes.byref(_err)) - _check_kv_error(_err) - return bool(_result) - - - @staticmethod - def default_capacity() -> int: - """The largest number of live entries one store will hold - """ - _fn = _lib.weaveffi_kv_Store_default_capacity - _fn.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_int64 - _err = _WeaveFFIErrorStruct() - _result = _fn(ctypes.byref(_err)) - _check_error(_err) - return _result - - - -# Fires when an entry is evicted from the store -# Callback type OnEvict: Callable[[str], None] -_CFUNC_weaveffi_kv_OnEvict_fn = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_void_p) - - -def register_eviction_listener(callback: Callable[[str], None]) -> int: - """ - Subscribe to per-key eviction notifications - - Returns a subscription id for unregister_eviction_listener(). - """ - def _trampoline(key, _context): - callback(_bytes_to_string(key)) - _cfunc = _CFUNC_weaveffi_kv_OnEvict_fn(_trampoline) - _fn = _lib.weaveffi_kv_register_eviction_listener - _fn.argtypes = [_CFUNC_weaveffi_kv_OnEvict_fn, ctypes.c_void_p] - _fn.restype = ctypes.c_uint64 - _listener_id = int(_fn(_cfunc, None)) - _listener_refs[_listener_id] = _cfunc - return _listener_id - - -def unregister_eviction_listener(listener_id: int) -> None: - """Unregister a listener previously registered with register_eviction_listener().""" - _fn = _lib.weaveffi_kv_unregister_eviction_listener - _fn.argtypes = [ctypes.c_uint64] - _fn.restype = None - _fn(ctypes.c_uint64(listener_id)) - _listener_refs.pop(listener_id, None) - - -# === Module: kv_stats === - -@dataclass -class Stats: - """Aggregate store statistics""" - - # Number of live entries - total_entries: int - # Sum of all value byte lengths - total_bytes: int - # Number of entries past their TTL but not yet evicted - expired_entries: int - - -def _write_Stats(_w: _BufferWriter, value: "Stats") -> None: - _w.write_i64(value.total_entries) - _w.write_i64(value.total_bytes) - _w.write_i64(value.expired_entries) - - -def _read_Stats(_r: _BufferReader) -> "Stats": - return Stats( - total_entries=_r.read_i64(), - total_bytes=_r.read_i64(), - expired_entries=_r.read_i64(), - ) - - -def _pack_Stats(value: "Stats") -> bytes: - _w = _BufferWriter() - _write_Stats(_w, value) - return _w.finish() - - -def _unpack_Stats(data: bytes) -> "Stats": - return _decode_buffer(data, _read_Stats) - - -def get_stats(store: "Store") -> "Stats": - """Snapshot the current store statistics - - Raises - ------ - KvError - If the call reports one of the domain's error codes. - """ - _fn = _lib.weaveffi_kv_stats_get_stats - _fn.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.POINTER(_WeaveFFIErrorStruct)] - _fn.restype = ctypes.c_void_p - _err = _WeaveFFIErrorStruct() - _out_len = ctypes.c_size_t(0) - _result = _fn(store._ptr, ctypes.byref(_out_len), ctypes.byref(_err)) - _check_kv_error(_err) - _data = _take_buffer(_result, _out_len.value) - return _unpack_Stats(_data) - -# END weaveffi.py diff --git a/crates/weaveffi-cli/tests/snapshots/python_kvstore__weaveffi_weaveffi_pyi.snap b/crates/weaveffi-cli/tests/snapshots/python_kvstore__weaveffi_weaveffi_pyi.snap deleted file mode 100644 index c2b38119..00000000 --- a/crates/weaveffi-cli/tests/snapshots/python_kvstore__weaveffi_weaveffi_pyi.snap +++ /dev/null @@ -1,102 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -from enum import IntEnum -from typing import Callable, Dict, Iterator, List, Optional, Type - -class WeaveFFIError(Exception): - code: int - message: str - def __init__(self, code: int, message: str) -> None: ... - -class KvError(WeaveFFIError): - KeyNotFound: Type["KeyNotFound"] - Expired: Type["Expired"] - StoreFull: Type["StoreFull"] - IoError: Type["IoError"] - def __init__(self, code: int, message: str) -> None: ... - -# No entry exists for the requested key -class KeyNotFound(KvError): - CODE: int - def __init__(self, message: str = ...) -> None: ... - -# The entry exists but its TTL has elapsed -class Expired(KvError): - CODE: int - def __init__(self, message: str = ...) -> None: ... - -# The store has reached its capacity limit -class StoreFull(KvError): - CODE: int - def __init__(self, message: str = ...) -> None: ... - -# Underlying storage reported an I/O failure -class IoError(KvError): - CODE: int - def __init__(self, message: str = ...) -> None: ... - -# Persistence semantics applied to a stored entry -class EntryKind(IntEnum): - # In-memory only; lost on close - Volatile: int - # Flushed to durable storage - Persistent: int - # Persistent and encrypted at rest - Encrypted: int - -# A single key-value entry persisted in the store -class Entry: - # Stable monotonic identifier assigned on insert - id: int - # UTF-8 lookup key - key: str - # Opaque binary payload - value: bytes - # Unix-timestamp seconds when the entry was created - created_at: int - # Optional unix-timestamp seconds at which the entry expires - expires_at: Optional[int] - # Free-form labels attached to the entry - tags: List[str] - # Arbitrary string-valued metadata pairs - metadata: Dict[str, str] - def __init__(self, id: int, key: str, value: bytes, created_at: int, expires_at: Optional[int], tags: List[str], metadata: Dict[str, str]) -> None: ... - -# An in-memory key-value store owning its entries; destroying the object releases all of its state -class Store: - @classmethod - def open(cls, path: str) -> "Store": ... - def put(self, key: str, value: bytes, kind: "EntryKind", ttl_seconds: Optional[int]) -> bool: ... - def get(self, key: str) -> Optional["Entry"]: ... - def delete(self, key: str) -> bool: ... - def list_keys(self, prefix: Optional[str]) -> Iterator[str]: ... - def count(self) -> int: ... - def clear(self) -> None: ... - async def compact(self) -> int: ... - def legacy_put(self, key: str, value: bytes) -> bool: ... - @staticmethod - def default_capacity() -> int: ... - -# Subscribe to per-key eviction notifications -def register_eviction_listener(callback: Callable[[str], None]) -> int: ... -def unregister_eviction_listener(listener_id: int) -> None: ... - -# Aggregate store statistics -class Stats: - # Number of live entries - total_entries: int - # Sum of all value byte lengths - total_bytes: int - # Number of entries past their TTL but not yet evicted - expired_entries: int - def __init__(self, total_entries: int, total_bytes: int, expired_entries: int) -> None: ... - -# Snapshot the current store statistics -def get_stats(store: "Store") -> "Stats": ... - -# END weaveffi.pyi diff --git a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__weaveffi_weaveffi_py.snap index e0b78be5..65925b23 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_nested_modules__weaveffi_weaveffi_py.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_nested_modules__weaveffi_weaveffi_py.snap @@ -49,6 +49,32 @@ def _load_library() -> ctypes.CDLL: _lib = _load_library() + +# The ABI revision these bindings were generated against. Checked before any +# other symbol is bound so a mismatched producer fails at import time instead +# of misreading the error struct or a value buffer later. +_ABI_VERSION = 1 + + +def _check_abi_version(lib: ctypes.CDLL) -> None: + try: + fn = lib.weaveffi_abi_version + except AttributeError: + raise ImportError( + "the loaded WeaveFFI library predates ABI versioning " + f"(these bindings expect ABI revision {_ABI_VERSION})" + ) from None + fn.argtypes = [] + fn.restype = ctypes.c_uint32 + found = fn() + if found != _ABI_VERSION: + raise ImportError( + f"WeaveFFI ABI mismatch: these bindings expect revision {_ABI_VERSION} " + f"but the loaded library reports revision {found}" + ) + + +_check_abi_version(_lib) _lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] _lib.weaveffi_error_clear.restype = None # Async completion callbacks receive a heap-boxed error the consumer owns; diff --git a/crates/weaveffi-cli/tests/snapshots/python_shapes__weaveffi_weaveffi_py.snap b/crates/weaveffi-cli/tests/snapshots/python_shapes__weaveffi_weaveffi_py.snap index ebd36397..00b70ead 100644 --- a/crates/weaveffi-cli/tests/snapshots/python_shapes__weaveffi_weaveffi_py.snap +++ b/crates/weaveffi-cli/tests/snapshots/python_shapes__weaveffi_weaveffi_py.snap @@ -49,6 +49,32 @@ def _load_library() -> ctypes.CDLL: _lib = _load_library() + +# The ABI revision these bindings were generated against. Checked before any +# other symbol is bound so a mismatched producer fails at import time instead +# of misreading the error struct or a value buffer later. +_ABI_VERSION = 1 + + +def _check_abi_version(lib: ctypes.CDLL) -> None: + try: + fn = lib.weaveffi_abi_version + except AttributeError: + raise ImportError( + "the loaded WeaveFFI library predates ABI versioning " + f"(these bindings expect ABI revision {_ABI_VERSION})" + ) from None + fn.argtypes = [] + fn.restype = ctypes.c_uint32 + found = fn() + if found != _ABI_VERSION: + raise ImportError( + f"WeaveFFI ABI mismatch: these bindings expect revision {_ABI_VERSION} " + f"but the loaded library reports revision {found}" + ) + + +_check_abi_version(_lib) _lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] _lib.weaveffi_error_clear.restype = None # Async completion callbacks receive a heap-boxed error the consumer owns; diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_async_demo__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_async_demo__lib_weaveffi_rb.snap deleted file mode 100644 index 3e229d3c..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_async_demo__lib_weaveffi_rb.snap +++ /dev/null @@ -1,426 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -# frozen_string_literal: true -# WeaveFFI Ruby FFI bindings (auto-generated) - -require 'ffi' - -module WeaveFFI - extend FFI::Library - - # An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a - # specific build artifact regardless of its file name or location. - _wv_override = ENV['WEAVEFFI_LIBRARY'] - if _wv_override && !_wv_override.empty? - ffi_lib _wv_override - else - case FFI::Platform::OS - when /darwin/ - ffi_lib 'libweaveffi.dylib' - when /mswin|mingw/ - ffi_lib 'weaveffi.dll' - else - ffi_lib 'libweaveffi.so' - end - end - - class ErrorStruct < FFI::Struct - layout :code, :int32, - :message, :pointer, - :payload_ptr, :pointer, - :payload_len, :size_t - end - - class Error < StandardError - attr_reader :code - - def initialize(code, message) - @code = code - super(message) - end - end - - # @api private - # Appends values in the WeaveFFI value-buffer wire format: little-endian, - # packed, no alignment. - class WvBufferWriter - def initialize - @buf = +''.b - end - - # The encoded bytes as a binary String. - def data - @buf - end - - def write_bool(v) - @buf << (v ? "\x01".b : "\x00".b) - end - - def write_flag(v) - write_bool(v) - end - - def write_i8(v) - @buf << [v].pack('c') - end - - def write_u8(v) - @buf << [v].pack('C') - end - - def write_i16(v) - @buf << [v].pack('s<') - end - - def write_u16(v) - @buf << [v].pack('S<') - end - - def write_i32(v) - @buf << [v].pack('l<') - end - - def write_u32(v) - @buf << [v].pack('L<') - end - - def write_len(v) - write_u32(v) - end - - def write_i64(v) - @buf << [v].pack('q<') - end - - def write_u64(v) - @buf << [v].pack('Q<') - end - - def write_f32(v) - @buf << [v].pack('e') - end - - def write_f64(v) - @buf << [v].pack('E') - end - - def write_string(v) - b = v.to_s.encode(Encoding::UTF_8).b - write_u32(b.bytesize) - @buf << b - end - - def write_bytes(v) - b = v.to_s.b - write_u32(b.bytesize) - @buf << b - end - end - - # @api private - # Reads values in the WeaveFFI value-buffer wire format, raising Error on - # any malformed buffer. - class WvBufferReader - def initialize(data) - @data = data.to_s.b - @pos = 0 - end - - def take(n, what) - raise Error.new(-1, "malformed value buffer: #{what}") if @pos + n > @data.bytesize - s = @data.byteslice(@pos, n) - @pos += n - s - end - - def read_bool - b = take(1, 'bool').unpack1('C') - raise Error.new(-1, 'malformed value buffer: bool byte out of range') if b > 1 - b == 1 - end - - def read_flag - b = take(1, 'option flag').unpack1('C') - raise Error.new(-1, 'malformed value buffer: option flag out of range') if b > 1 - b == 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 - - def read_len - len = read_u32 - if len > @data.bytesize - @pos - raise Error.new(-1, 'malformed value buffer: length prefix exceeds remaining bytes') - end - len - 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 - s = take(read_len, 'string bytes').force_encoding(Encoding::UTF_8) - raise Error.new(-1, 'malformed value buffer: string is not valid UTF-8') unless s.valid_encoding? - s - end - - def read_bytes - take(read_len, 'byte buffer') - end - - def expect_end! - return if @pos == @data.bytesize - raise Error.new(-1, 'malformed value buffer: trailing bytes after value') - end - end - - attach_function :weaveffi_error_clear, [:pointer], :void - attach_function :weaveffi_error_free, [:pointer], :void - attach_function :weaveffi_free_string, [:pointer], :void - attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void - - def self.check_error!(err) - return if err[:code].zero? - code = err[:code] - msg_ptr = err[:message] - msg = msg_ptr.null? ? '' : msg_ptr.read_string - weaveffi_error_clear(err.to_ptr) - raise Error.new(code, msg) - end - - # === Module: tasks === - - # Base error for the `tasks` module's error domain. - class TaskError < Error - # task name must not be empty - class InvalidName < TaskError - CODE = 1 - - def initialize(message = 'task name must not be empty') - super(1, message) - end - end - end - - # Maps each ABI code of the TaskError domain to its error class. - TASK_ERROR_CODES = { - 1 => TaskError::InvalidName, - }.freeze - - # Builds the TaskError subclass matching `code`, decoding any payload - # fields the code declares, or a generic Error for codes outside - # the domain (panics, marshalling). - def self.task_error_from(code, message, payload = nil) - cls = TASK_ERROR_CODES[code] - return Error.new(code, message) if cls.nil? - message.empty? ? cls.new : cls.new(message) - end - - # Raises the typed TaskError for a non-zero error slot. - def self.check_task_error!(err) - return if err[:code].zero? - code = err[:code] - msg_ptr = err[:message] - msg = msg_ptr.null? ? '' : msg_ptr.read_string - payload_ptr = err[:payload_ptr] - payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) - weaveffi_error_clear(err.to_ptr) - raise task_error_from(code, msg, payload) - end - - class TaskResult - attr_reader :id - - attr_reader :value - - attr_reader :success - - def initialize(id:, value:, success:) - @id = id - @value = value - @success = success - end - - # Structural equality over every field. - def ==(other) - return false unless other.is_a?(TaskResult) - return false unless id == other.id - return false unless value == other.value - return false unless success == other.success - true - end - end - - # @api private - # Packs a TaskResult into the value-buffer wire format. - def self._wv_write_task_result(w, v) - w.write_i64(v.id) - w.write_string(v.value) - w.write_bool(v.success) - end - - # @api private - # Unpacks a TaskResult from the value-buffer wire format. - def self._wv_read_task_result(r) - _wv_id = r.read_i64 - _wv_value = r.read_string - _wv_success = r.read_bool - TaskResult.new(id: _wv_id, value: _wv_value, success: _wv_success) - end - callback :weaveffi_tasks_run_task_callback, [:pointer, :pointer, :pointer, :size_t], :void - attach_function :weaveffi_tasks_run_task_async, [:string, :weaveffi_tasks_run_task_callback, :pointer], :void - callback :weaveffi_tasks_run_batch_callback, [:pointer, :pointer, :pointer, :size_t], :void - attach_function :weaveffi_tasks_run_batch_async, [:pointer, :size_t, :weaveffi_tasks_run_batch_callback, :pointer], :void - attach_function :weaveffi_tasks_cancel_task, [:int64, :pointer], :int32 - callback :weaveffi_tasks_run_n_tasks_callback, [:pointer, :pointer, :int32], :void - attach_function :weaveffi_tasks_run_n_tasks_async, [:int32, :weaveffi_tasks_run_n_tasks_callback, :pointer], :void - attach_function :weaveffi_tasks_active_callbacks, [:pointer], :int64 - - # Blocks the current thread until the async producer completes; the - # result (or error) is delivered through the completion callback. - def self.run_task(name) - queue = Queue.new - callback = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |_context, err_ptr, result_ptr, result_len| - err = err_ptr.null? ? nil : ErrorStruct.new(err_ptr) - if err && err[:code] != 0 - code = err[:code] - msg = err[:message].null? ? '' : err[:message].read_string - payload = err[:payload_ptr].null? ? nil : err[:payload_ptr].read_string(err[:payload_len]) - weaveffi_error_free(err_ptr) - queue << task_error_from(code, msg, payload) - else - begin - _wv_r = WvBufferReader.new(result_ptr.null? ? ''.b : result_ptr.read_string(result_len)) - weaveffi_free_bytes(result_ptr, result_len) unless result_ptr.null? - _wv_v = _wv_read_task_result(_wv_r) - _wv_r.expect_end! - queue << _wv_v - rescue Error => e - queue << e - end - end - end - weaveffi_tasks_run_task_async(name, callback, FFI::Pointer::NULL) - value = queue.pop - raise value if value.is_a?(Error) - value - end - - # Blocks the current thread until the async producer completes; the - # result (or error) is delivered through the completion callback. - def self.run_batch(names) - queue = Queue.new - callback = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |_context, err_ptr, result_ptr, result_len| - err = err_ptr.null? ? nil : ErrorStruct.new(err_ptr) - if err && err[:code] != 0 - code = err[:code] - msg = err[:message].null? ? '' : err[:message].read_string - weaveffi_error_free(err_ptr) - queue << Error.new(code, msg) - else - begin - _wv_r = WvBufferReader.new(result_ptr.null? ? ''.b : result_ptr.read_string(result_len)) - weaveffi_free_bytes(result_ptr, result_len) unless result_ptr.null? - _wv_v = Array.new(_wv_r.read_len) do - _wv_e0 = _wv_read_task_result(_wv_r) - _wv_e0 - end - _wv_r.expect_end! - queue << _wv_v - rescue Error => e - queue << e - end - end - end - names_w = WvBufferWriter.new - names_w.write_len(names.length) - names.each do |_wv_e0| - names_w.write_string(_wv_e0) - end - names_data = names_w.data - names_buf = FFI::MemoryPointer.new(:uint8, names_data.bytesize) - names_buf.put_bytes(0, names_data) - weaveffi_tasks_run_batch_async(names_buf, names_data.bytesize, callback, FFI::Pointer::NULL) - value = queue.pop - raise value if value.is_a?(Error) - value - end - - def self.cancel_task(id) - err = ErrorStruct.new - result = weaveffi_tasks_cancel_task(id, err) - check_error!(err) - result != 0 - end - - # Blocks the current thread until the async producer completes; the - # result (or error) is delivered through the completion callback. - def self.run_n_tasks(n) - queue = Queue.new - callback = FFI::Function.new(:void, [:pointer, :pointer, :int32]) do |_context, err_ptr, result| - err = err_ptr.null? ? nil : ErrorStruct.new(err_ptr) - if err && err[:code] != 0 - code = err[:code] - msg = err[:message].null? ? '' : err[:message].read_string - weaveffi_error_free(err_ptr) - queue << Error.new(code, msg) - else - queue << result - end - end - weaveffi_tasks_run_n_tasks_async(n, callback, FFI::Pointer::NULL) - value = queue.pop - raise value if value.is_a?(Error) - value - end - - def self.active_callbacks() - err = ErrorStruct.new - result = weaveffi_tasks_active_callbacks(err) - check_error!(err) - result - end -end - -# END weaveffi.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_calculator__README_md.snap b/crates/weaveffi-cli/tests/snapshots/ruby_calculator__README_md.snap deleted file mode 100644 index dfa27948..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_calculator__README_md.snap +++ /dev/null @@ -1,30 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# weaveffi (Ruby) - -Auto-generated Ruby bindings using the [ffi](https://github.com/ffi/ffi) gem. - -## Prerequisites - -- Ruby >= 2.7 -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path. - -## Install - -```bash -gem build weaveffi.gemspec -gem install weaveffi-0.1.0.gem -``` - -## Usage - -```ruby -require 'weaveffi' -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_calculator__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_calculator__lib_weaveffi_rb.snap deleted file mode 100644 index 558dd27e..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_calculator__lib_weaveffi_rb.snap +++ /dev/null @@ -1,316 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -# frozen_string_literal: true -# WeaveFFI Ruby FFI bindings (auto-generated) - -require 'ffi' - -module WeaveFFI - extend FFI::Library - - # An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a - # specific build artifact regardless of its file name or location. - _wv_override = ENV['WEAVEFFI_LIBRARY'] - if _wv_override && !_wv_override.empty? - ffi_lib _wv_override - else - case FFI::Platform::OS - when /darwin/ - ffi_lib 'libweaveffi.dylib' - when /mswin|mingw/ - ffi_lib 'weaveffi.dll' - else - ffi_lib 'libweaveffi.so' - end - end - - class ErrorStruct < FFI::Struct - layout :code, :int32, - :message, :pointer, - :payload_ptr, :pointer, - :payload_len, :size_t - end - - class Error < StandardError - attr_reader :code - - def initialize(code, message) - @code = code - super(message) - end - end - - # @api private - # Appends values in the WeaveFFI value-buffer wire format: little-endian, - # packed, no alignment. - class WvBufferWriter - def initialize - @buf = +''.b - end - - # The encoded bytes as a binary String. - def data - @buf - end - - def write_bool(v) - @buf << (v ? "\x01".b : "\x00".b) - end - - def write_flag(v) - write_bool(v) - end - - def write_i8(v) - @buf << [v].pack('c') - end - - def write_u8(v) - @buf << [v].pack('C') - end - - def write_i16(v) - @buf << [v].pack('s<') - end - - def write_u16(v) - @buf << [v].pack('S<') - end - - def write_i32(v) - @buf << [v].pack('l<') - end - - def write_u32(v) - @buf << [v].pack('L<') - end - - def write_len(v) - write_u32(v) - end - - def write_i64(v) - @buf << [v].pack('q<') - end - - def write_u64(v) - @buf << [v].pack('Q<') - end - - def write_f32(v) - @buf << [v].pack('e') - end - - def write_f64(v) - @buf << [v].pack('E') - end - - def write_string(v) - b = v.to_s.encode(Encoding::UTF_8).b - write_u32(b.bytesize) - @buf << b - end - - def write_bytes(v) - b = v.to_s.b - write_u32(b.bytesize) - @buf << b - end - end - - # @api private - # Reads values in the WeaveFFI value-buffer wire format, raising Error on - # any malformed buffer. - class WvBufferReader - def initialize(data) - @data = data.to_s.b - @pos = 0 - end - - def take(n, what) - raise Error.new(-1, "malformed value buffer: #{what}") if @pos + n > @data.bytesize - s = @data.byteslice(@pos, n) - @pos += n - s - end - - def read_bool - b = take(1, 'bool').unpack1('C') - raise Error.new(-1, 'malformed value buffer: bool byte out of range') if b > 1 - b == 1 - end - - def read_flag - b = take(1, 'option flag').unpack1('C') - raise Error.new(-1, 'malformed value buffer: option flag out of range') if b > 1 - b == 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 - - def read_len - len = read_u32 - if len > @data.bytesize - @pos - raise Error.new(-1, 'malformed value buffer: length prefix exceeds remaining bytes') - end - len - 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 - s = take(read_len, 'string bytes').force_encoding(Encoding::UTF_8) - raise Error.new(-1, 'malformed value buffer: string is not valid UTF-8') unless s.valid_encoding? - s - end - - def read_bytes - take(read_len, 'byte buffer') - end - - def expect_end! - return if @pos == @data.bytesize - raise Error.new(-1, 'malformed value buffer: trailing bytes after value') - end - end - - attach_function :weaveffi_error_clear, [:pointer], :void - attach_function :weaveffi_error_free, [:pointer], :void - attach_function :weaveffi_free_string, [:pointer], :void - attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void - - def self.check_error!(err) - return if err[:code].zero? - code = err[:code] - msg_ptr = err[:message] - msg = msg_ptr.null? ? '' : msg_ptr.read_string - weaveffi_error_clear(err.to_ptr) - raise Error.new(code, msg) - end - - # === Module: calculator === - - # Base error for the `calculator` module's error domain. - class CalcError < Error - # division by zero - class DivisionByZero < CalcError - CODE = 1 - - def initialize(message = 'division by zero') - super(1, message) - end - end - end - - # Maps each ABI code of the CalcError domain to its error class. - CALC_ERROR_CODES = { - 1 => CalcError::DivisionByZero, - }.freeze - - # Builds the CalcError subclass matching `code`, decoding any payload - # fields the code declares, or a generic Error for codes outside - # the domain (panics, marshalling). - def self.calc_error_from(code, message, payload = nil) - cls = CALC_ERROR_CODES[code] - return Error.new(code, message) if cls.nil? - message.empty? ? cls.new : cls.new(message) - end - - # Raises the typed CalcError for a non-zero error slot. - def self.check_calc_error!(err) - return if err[:code].zero? - code = err[:code] - msg_ptr = err[:message] - msg = msg_ptr.null? ? '' : msg_ptr.read_string - payload_ptr = err[:payload_ptr] - payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) - weaveffi_error_clear(err.to_ptr) - raise calc_error_from(code, msg, payload) - end - # Add two integers - attach_function :weaveffi_calculator_add, [:int32, :int32, :pointer], :int32 - # Multiply two integers - attach_function :weaveffi_calculator_mul, [:int32, :int32, :pointer], :int32 - # Divide two integers, failing on a zero divisor - attach_function :weaveffi_calculator_div, [:int32, :int32, :pointer], :int32 - # Echo a string back - attach_function :weaveffi_calculator_echo, [:string, :pointer], :pointer - - # Add two integers - def self.add(a, b) - err = ErrorStruct.new - result = weaveffi_calculator_add(a, b, err) - check_error!(err) - result - end - - # Multiply two integers - def self.mul(a, b) - err = ErrorStruct.new - result = weaveffi_calculator_mul(a, b, err) - check_error!(err) - result - end - - # Divide two integers, failing on a zero divisor - def self.div(a, b) - err = ErrorStruct.new - result = weaveffi_calculator_div(a, b, err) - check_calc_error!(err) - result - end - - # Echo a string back - def self.echo(s) - err = ErrorStruct.new - result = weaveffi_calculator_echo(s, err) - check_error!(err) - return '' if result.null? - str = result.read_string - weaveffi_free_string(result) - str - end -end - -# END weaveffi.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_calculator__weaveffi_gemspec.snap b/crates/weaveffi-cli/tests/snapshots/ruby_calculator__weaveffi_gemspec.snap deleted file mode 100644 index c132abf7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_calculator__weaveffi_gemspec.snap +++ /dev/null @@ -1,18 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -Gem::Specification.new do |s| - s.name = 'weaveffi' - s.version = '0.1.0' - s.summary = 'weaveffi bindings generated by WeaveFFI' - s.files = Dir['lib/**/*.rb'] - s.require_paths = ['lib'] - - s.add_dependency 'ffi', '~> 1.15' -end - -# END weaveffi.gemspec diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_contacts__README_md.snap b/crates/weaveffi-cli/tests/snapshots/ruby_contacts__README_md.snap deleted file mode 100644 index dfa27948..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_contacts__README_md.snap +++ /dev/null @@ -1,30 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# weaveffi (Ruby) - -Auto-generated Ruby bindings using the [ffi](https://github.com/ffi/ffi) gem. - -## Prerequisites - -- Ruby >= 2.7 -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path. - -## Install - -```bash -gem build weaveffi.gemspec -gem install weaveffi-0.1.0.gem -``` - -## Usage - -```ruby -require 'weaveffi' -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_contacts__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_contacts__lib_weaveffi_rb.snap deleted file mode 100644 index 5343fd0f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_contacts__lib_weaveffi_rb.snap +++ /dev/null @@ -1,460 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -# frozen_string_literal: true -# WeaveFFI Ruby FFI bindings (auto-generated) - -require 'ffi' - -module WeaveFFI - extend FFI::Library - - # An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a - # specific build artifact regardless of its file name or location. - _wv_override = ENV['WEAVEFFI_LIBRARY'] - if _wv_override && !_wv_override.empty? - ffi_lib _wv_override - else - case FFI::Platform::OS - when /darwin/ - ffi_lib 'libweaveffi.dylib' - when /mswin|mingw/ - ffi_lib 'weaveffi.dll' - else - ffi_lib 'libweaveffi.so' - end - end - - class ErrorStruct < FFI::Struct - layout :code, :int32, - :message, :pointer, - :payload_ptr, :pointer, - :payload_len, :size_t - end - - class Error < StandardError - attr_reader :code - - def initialize(code, message) - @code = code - super(message) - end - end - - # @api private - # Appends values in the WeaveFFI value-buffer wire format: little-endian, - # packed, no alignment. - class WvBufferWriter - def initialize - @buf = +''.b - end - - # The encoded bytes as a binary String. - def data - @buf - end - - def write_bool(v) - @buf << (v ? "\x01".b : "\x00".b) - end - - def write_flag(v) - write_bool(v) - end - - def write_i8(v) - @buf << [v].pack('c') - end - - def write_u8(v) - @buf << [v].pack('C') - end - - def write_i16(v) - @buf << [v].pack('s<') - end - - def write_u16(v) - @buf << [v].pack('S<') - end - - def write_i32(v) - @buf << [v].pack('l<') - end - - def write_u32(v) - @buf << [v].pack('L<') - end - - def write_len(v) - write_u32(v) - end - - def write_i64(v) - @buf << [v].pack('q<') - end - - def write_u64(v) - @buf << [v].pack('Q<') - end - - def write_f32(v) - @buf << [v].pack('e') - end - - def write_f64(v) - @buf << [v].pack('E') - end - - def write_string(v) - b = v.to_s.encode(Encoding::UTF_8).b - write_u32(b.bytesize) - @buf << b - end - - def write_bytes(v) - b = v.to_s.b - write_u32(b.bytesize) - @buf << b - end - end - - # @api private - # Reads values in the WeaveFFI value-buffer wire format, raising Error on - # any malformed buffer. - class WvBufferReader - def initialize(data) - @data = data.to_s.b - @pos = 0 - end - - def take(n, what) - raise Error.new(-1, "malformed value buffer: #{what}") if @pos + n > @data.bytesize - s = @data.byteslice(@pos, n) - @pos += n - s - end - - def read_bool - b = take(1, 'bool').unpack1('C') - raise Error.new(-1, 'malformed value buffer: bool byte out of range') if b > 1 - b == 1 - end - - def read_flag - b = take(1, 'option flag').unpack1('C') - raise Error.new(-1, 'malformed value buffer: option flag out of range') if b > 1 - b == 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 - - def read_len - len = read_u32 - if len > @data.bytesize - @pos - raise Error.new(-1, 'malformed value buffer: length prefix exceeds remaining bytes') - end - len - 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 - s = take(read_len, 'string bytes').force_encoding(Encoding::UTF_8) - raise Error.new(-1, 'malformed value buffer: string is not valid UTF-8') unless s.valid_encoding? - s - end - - def read_bytes - take(read_len, 'byte buffer') - end - - def expect_end! - return if @pos == @data.bytesize - raise Error.new(-1, 'malformed value buffer: trailing bytes after value') - end - end - - attach_function :weaveffi_error_clear, [:pointer], :void - attach_function :weaveffi_error_free, [:pointer], :void - attach_function :weaveffi_free_string, [:pointer], :void - attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void - - def self.check_error!(err) - return if err[:code].zero? - code = err[:code] - msg_ptr = err[:message] - msg = msg_ptr.null? ? '' : msg_ptr.read_string - weaveffi_error_clear(err.to_ptr) - raise Error.new(code, msg) - end - - # === Module: contacts === - - # Base error for the `contacts` module's error domain. - class ContactsError < Error - # name must not be empty - class InvalidName < ContactsError - CODE = 1 - - def initialize(message = 'name must not be empty') - super(1, message) - end - end - - # contact not found - class NotFound < ContactsError - CODE = 2 - - def initialize(message = 'contact not found') - super(2, message) - end - end - end - - # Maps each ABI code of the ContactsError domain to its error class. - CONTACTS_ERROR_CODES = { - 1 => ContactsError::InvalidName, - 2 => ContactsError::NotFound, - }.freeze - - # Builds the ContactsError subclass matching `code`, decoding any payload - # fields the code declares, or a generic Error for codes outside - # the domain (panics, marshalling). - def self.contacts_error_from(code, message, payload = nil) - cls = CONTACTS_ERROR_CODES[code] - return Error.new(code, message) if cls.nil? - message.empty? ? cls.new : cls.new(message) - end - - # Raises the typed ContactsError for a non-zero error slot. - def self.check_contacts_error!(err) - return if err[:code].zero? - code = err[:code] - msg_ptr = err[:message] - msg = msg_ptr.null? ? '' : msg_ptr.read_string - payload_ptr = err[:payload_ptr] - payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) - weaveffi_error_clear(err.to_ptr) - raise contacts_error_from(code, msg, payload) - end - - module ContactType - PERSONAL = 0 - WORK = 1 - OTHER = 2 - end - - class Contact - attr_reader :id - - attr_reader :first_name - - attr_reader :last_name - - attr_reader :email - - attr_reader :contact_type - - def initialize(id:, first_name:, last_name:, email:, contact_type:) - @id = id - @first_name = first_name - @last_name = last_name - @email = email - @contact_type = contact_type - end - - # Structural equality over every field. - def ==(other) - return false unless other.is_a?(Contact) - return false unless id == other.id - return false unless first_name == other.first_name - return false unless last_name == other.last_name - return false unless email == other.email - return false unless contact_type == other.contact_type - true - end - end - - # @api private - # Packs a Contact into the value-buffer wire format. - def self._wv_write_contact(w, v) - w.write_i64(v.id) - w.write_string(v.first_name) - w.write_string(v.last_name) - if v.email.nil? - w.write_flag(false) - else - w.write_flag(true) - w.write_string(v.email) - end - w.write_i32(v.contact_type) - end - - # @api private - # Unpacks a Contact from the value-buffer wire format. - def self._wv_read_contact(r) - _wv_id = r.read_i64 - _wv_first_name = r.read_string - _wv_last_name = r.read_string - if r.read_flag - _wv_email = r.read_string - else - _wv_email = nil - end - _wv_contact_type = r.read_i32 - Contact.new(id: _wv_id, first_name: _wv_first_name, last_name: _wv_last_name, email: _wv_email, contact_type: _wv_contact_type) - end - - attach_function :weaveffi_contacts_ContactBook_destroy, [:pointer], :void - attach_function :weaveffi_contacts_ContactBook_new, [:pointer], :pointer - attach_function :weaveffi_contacts_ContactBook_add, [:pointer, :string, :string, :pointer, :size_t, :int32, :pointer, :pointer], :pointer - attach_function :weaveffi_contacts_ContactBook_get, [:pointer, :int64, :pointer, :pointer], :pointer - attach_function :weaveffi_contacts_ContactBook_list, [:pointer, :pointer, :pointer], :pointer - attach_function :weaveffi_contacts_ContactBook_remove, [:pointer, :int64, :pointer], :int32 - attach_function :weaveffi_contacts_ContactBook_count, [:pointer, :pointer], :int32 - - class ContactBookPtr < FFI::AutoPointer - def self.release(ptr) - WeaveFFI.weaveffi_contacts_ContactBook_destroy(ptr) - end - end - - # An in-memory address book owning its contacts - class ContactBook - attr_reader :handle - - # Wraps an owned pointer the producer handed over, without - # re-running initialize. - def self._from_ptr(ptr) - obj = allocate - obj.instance_variable_set(:@handle, ContactBookPtr.new(ptr)) - obj - end - - def destroy - return if @handle.nil? - @handle.free - @handle = nil - end - - def initialize() - err = ErrorStruct.new - result = WeaveFFI.weaveffi_contacts_ContactBook_new(err) - WeaveFFI.check_error!(err) - raise Error.new(-1, 'null pointer') if result.null? - @handle = ContactBookPtr.new(result) - end - - def add(first_name, last_name, email, contact_type) - err = ErrorStruct.new - email_w = WvBufferWriter.new - if email.nil? - email_w.write_flag(false) - else - email_w.write_flag(true) - email_w.write_string(email) - end - email_data = email_w.data - email_buf = FFI::MemoryPointer.new(:uint8, email_data.bytesize) - email_buf.put_bytes(0, email_data) - out_len = FFI::MemoryPointer.new(:size_t) - result = WeaveFFI.weaveffi_contacts_ContactBook_add(@handle, first_name, last_name, email_buf, email_data.bytesize, contact_type, out_len, err) - WeaveFFI.check_contacts_error!(err) - len = out_len.read(:size_t) - data = result.null? ? ''.b : result.read_string(len) - WeaveFFI.weaveffi_free_bytes(result, len) unless result.null? - _wv_r = WvBufferReader.new(data) - _wv_value = WeaveFFI._wv_read_contact(_wv_r) - _wv_r.expect_end! - _wv_value - end - - def get(id) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = WeaveFFI.weaveffi_contacts_ContactBook_get(@handle, id, out_len, err) - WeaveFFI.check_contacts_error!(err) - len = out_len.read(:size_t) - data = result.null? ? ''.b : result.read_string(len) - WeaveFFI.weaveffi_free_bytes(result, len) unless result.null? - _wv_r = WvBufferReader.new(data) - _wv_value = WeaveFFI._wv_read_contact(_wv_r) - _wv_r.expect_end! - _wv_value - end - - def list() - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = WeaveFFI.weaveffi_contacts_ContactBook_list(@handle, out_len, err) - WeaveFFI.check_error!(err) - len = out_len.read(:size_t) - data = result.null? ? ''.b : result.read_string(len) - WeaveFFI.weaveffi_free_bytes(result, len) unless result.null? - _wv_r = WvBufferReader.new(data) - _wv_value = Array.new(_wv_r.read_len) do - _wv_e0 = WeaveFFI._wv_read_contact(_wv_r) - _wv_e0 - end - _wv_r.expect_end! - _wv_value - end - - def remove(id) - err = ErrorStruct.new - result = WeaveFFI.weaveffi_contacts_ContactBook_remove(@handle, id, err) - WeaveFFI.check_error!(err) - result != 0 - end - - def count() - err = ErrorStruct.new - result = WeaveFFI.weaveffi_contacts_ContactBook_count(@handle, err) - WeaveFFI.check_error!(err) - result - end - end -end - -# END weaveffi.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_contacts__weaveffi_gemspec.snap b/crates/weaveffi-cli/tests/snapshots/ruby_contacts__weaveffi_gemspec.snap deleted file mode 100644 index c132abf7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_contacts__weaveffi_gemspec.snap +++ /dev/null @@ -1,18 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -Gem::Specification.new do |s| - s.name = 'weaveffi' - s.version = '0.1.0' - s.summary = 'weaveffi bindings generated by WeaveFFI' - s.files = Dir['lib/**/*.rb'] - s.require_paths = ['lib'] - - s.add_dependency 'ffi', '~> 1.15' -end - -# END weaveffi.gemspec diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_weaveffi_rb.snap index 49bc48f4..905ee6a5 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_weaveffi_rb.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_docs_everywhere__lib_weaveffi_rb.snap @@ -29,6 +29,22 @@ module WeaveFFI end end + # The ABI revision these bindings were generated against. Checked before any + # other symbol is attached so a mismatched producer fails at require time + # instead of misreading the error struct or a value buffer later. + ABI_VERSION = 1 + begin + attach_function :weaveffi_abi_version, [], :uint32 + rescue FFI::NotFoundError + raise LoadError, 'the loaded WeaveFFI library predates ABI versioning ' \ + "(these bindings expect ABI revision #{ABI_VERSION})" + end + _wv_abi = weaveffi_abi_version + unless _wv_abi == ABI_VERSION + raise LoadError, "WeaveFFI ABI mismatch: these bindings expect revision #{ABI_VERSION} " \ + "but the loaded library reports revision #{_wv_abi}" + end + class ErrorStruct < FFI::Struct layout :code, :int32, :message, :pointer, diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_async_demo__README_md.snap b/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__README_md.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/ruby_async_demo__README_md.snap rename to crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__README_md.snap diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__lib_weaveffi_rb.snap new file mode 100644 index 00000000..f0d5ec47 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__lib_weaveffi_rb.snap @@ -0,0 +1,1731 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +# Generated by WeaveFFI [VERSION] from weaveffi.yml +# DO NOT EDIT. Your changes will be overwritten. +# To regenerate: weaveffi generate weaveffi.yml -o + +# frozen_string_literal: true +# WeaveFFI Ruby FFI bindings (auto-generated) + +require 'ffi' + +module WeaveFFI + extend FFI::Library + + # An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a + # specific build artifact regardless of its file name or location. + _wv_override = ENV['WEAVEFFI_LIBRARY'] + if _wv_override && !_wv_override.empty? + ffi_lib _wv_override + else + case FFI::Platform::OS + when /darwin/ + ffi_lib 'libweaveffi.dylib' + when /mswin|mingw/ + ffi_lib 'weaveffi.dll' + else + ffi_lib 'libweaveffi.so' + end + end + + # The ABI revision these bindings were generated against. Checked before any + # other symbol is attached so a mismatched producer fails at require time + # instead of misreading the error struct or a value buffer later. + ABI_VERSION = 1 + begin + attach_function :weaveffi_abi_version, [], :uint32 + rescue FFI::NotFoundError + raise LoadError, 'the loaded WeaveFFI library predates ABI versioning ' \ + "(these bindings expect ABI revision #{ABI_VERSION})" + end + _wv_abi = weaveffi_abi_version + unless _wv_abi == ABI_VERSION + raise LoadError, "WeaveFFI ABI mismatch: these bindings expect revision #{ABI_VERSION} " \ + "but the loaded library reports revision #{_wv_abi}" + end + + class ErrorStruct < FFI::Struct + layout :code, :int32, + :message, :pointer, + :payload_ptr, :pointer, + :payload_len, :size_t + end + + class Error < StandardError + attr_reader :code + + def initialize(code, message) + @code = code + super(message) + end + end + + # @api private + # Appends values in the WeaveFFI value-buffer wire format: little-endian, + # packed, no alignment. + class WvBufferWriter + def initialize + @buf = +''.b + end + + # The encoded bytes as a binary String. + def data + @buf + end + + def write_bool(v) + @buf << (v ? "\x01".b : "\x00".b) + end + + def write_flag(v) + write_bool(v) + end + + def write_i8(v) + @buf << [v].pack('c') + end + + def write_u8(v) + @buf << [v].pack('C') + end + + def write_i16(v) + @buf << [v].pack('s<') + end + + def write_u16(v) + @buf << [v].pack('S<') + end + + def write_i32(v) + @buf << [v].pack('l<') + end + + def write_u32(v) + @buf << [v].pack('L<') + end + + def write_len(v) + write_u32(v) + end + + def write_i64(v) + @buf << [v].pack('q<') + end + + def write_u64(v) + @buf << [v].pack('Q<') + end + + def write_f32(v) + @buf << [v].pack('e') + end + + def write_f64(v) + @buf << [v].pack('E') + end + + def write_string(v) + b = v.to_s.encode(Encoding::UTF_8).b + write_u32(b.bytesize) + @buf << b + end + + def write_bytes(v) + b = v.to_s.b + write_u32(b.bytesize) + @buf << b + end + end + + # @api private + # Reads values in the WeaveFFI value-buffer wire format, raising Error on + # any malformed buffer. + class WvBufferReader + def initialize(data) + @data = data.to_s.b + @pos = 0 + end + + def take(n, what) + raise Error.new(-1, "malformed value buffer: #{what}") if @pos + n > @data.bytesize + s = @data.byteslice(@pos, n) + @pos += n + s + end + + def read_bool + b = take(1, 'bool').unpack1('C') + raise Error.new(-1, 'malformed value buffer: bool byte out of range') if b > 1 + b == 1 + end + + def read_flag + b = take(1, 'option flag').unpack1('C') + raise Error.new(-1, 'malformed value buffer: option flag out of range') if b > 1 + b == 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 + + def read_len + len = read_u32 + if len > @data.bytesize - @pos + raise Error.new(-1, 'malformed value buffer: length prefix exceeds remaining bytes') + end + len + 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 + s = take(read_len, 'string bytes').force_encoding(Encoding::UTF_8) + raise Error.new(-1, 'malformed value buffer: string is not valid UTF-8') unless s.valid_encoding? + s + end + + def read_bytes + take(read_len, 'byte buffer') + end + + def expect_end! + return if @pos == @data.bytesize + raise Error.new(-1, 'malformed value buffer: trailing bytes after value') + end + end + + attach_function :weaveffi_error_clear, [:pointer], :void + attach_function :weaveffi_error_free, [:pointer], :void + attach_function :weaveffi_free_string, [:pointer], :void + attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void + + def self.check_error!(err) + return if err[:code].zero? + code = err[:code] + msg_ptr = err[:message] + msg = msg_ptr.null? ? '' : msg_ptr.read_string + weaveffi_error_clear(err.to_ptr) + raise Error.new(code, msg) + end + + # Registered listener trampolines, keyed by subscription id. Holding the + # FFI::Function objects here keeps them alive until unregistered; without + # this the GC could collect a trampoline the producer still calls. + @listener_refs = {} + + # === 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 = 'Resource is busy') + super(7, message) + end + end + end + + # Maps each ABI code of the EdgeErrorsError domain to its error class. + EDGE_ERRORS_ERROR_CODES = { + 7 => EdgeErrorsError::Busy, + }.freeze + + # Builds the EdgeErrorsError subclass matching `code`, decoding any payload + # fields the code declares, or a generic Error for codes outside + # the domain (panics, marshalling). + def self.edge_errors_error_from(code, message, payload = nil) + cls = EDGE_ERRORS_ERROR_CODES[code] + return Error.new(code, message) if cls.nil? + message.empty? ? cls.new : cls.new(message) + end + + # Raises the typed EdgeErrorsError for a non-zero error slot. + def self.check_edge_errors_error!(err) + return if err[:code].zero? + code = err[:code] + msg_ptr = err[:message] + msg = msg_ptr.null? ? '' : msg_ptr.read_string + payload_ptr = err[:payload_ptr] + payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) + weaveffi_error_clear(err.to_ptr) + raise edge_errors_error_from(code, msg, payload) + end + + # Deprecated enum with non-contiguous values and keyword-like variants + 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) + return false unless other.is_a?(Leaf) + return false unless weight == other.weight + true + 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) + return false unless other.is_a?(Inner) + return false unless leaf == other.leaf + return false unless leaves == other.leaves + true + 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) + return false unless other.is_a?(Outer) + return false unless inner == other.inner + return false unless maybe_inner == other.maybe_inner + return false unless by_name == other.by_name + return false unless modes == other.modes + return false unless matrix == other.matrix + return false unless sparse == other.sparse + return false unless blob == other.blob + return false unless note == other.note + return false unless by_id == other.by_id + return false unless _hidden == other._hidden + return false unless v2 == other.v2 + true + 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) + return false unless other.is_a?(Keywords) + return false unless class_ == other.class_ + return false unless def_ == other.def_ + return false unless import == other.import + return false unless from == other.from + return false unless func == other.func + return false unless range == other.range + return false unless register == other.register + return false unless volatile == other.volatile + return false unless new == other.new + return false unless default == other.default + return false unless end_ == other.end_ + return false unless in_ == other.in_ + return false unless is == other.is + return false unless int == other.int + return false unless var == other.var + return false unless val == other.val + return false unless self_ == other.self_ + return false unless this == other.this + return false unless object == other.object + return false unless string == other.string + return false unless case_ == other.case_ + return false unless select == other.select + return false unless switch == other.switch + return false unless void == other.void + return false unless union == other.union + return false unless namespace == other.namespace + return false unless operator == other.operator + return false unless template == other.template + return false unless package == other.package + return false unless protocol == other.protocol + return false unless guard == other.guard + return false unless defer == other.defer + return false unless inout == other.inout + return false unless internal == other.internal + return false unless when_ == other.when_ + return false unless fun == other.fun + return false unless data == other.data + return false unless late == other.late + return false unless part == other.part + return false unless out == other.out + return false unless ref == other.ref + return false unless event == other.event + return false unless base == other.base + return false unless lambda == other.lambda + return false unless yield_ == other.yield_ + return false unless nil_ == other.nil_ + return false unless null == other.null + return false unless begin_ == other.begin_ + return false unless rescue_ == other.rescue_ + return false unless module_ == other.module_ + return false unless chan == other.chan + return false unless go == other.go + return false unless let == other.let + return false unless export == other.export + return false unless delete == other.delete + return false unless typedef == other.typedef + return false unless sizeof == other.sizeof + return false unless static == other.static + return false unless super_ == other.super_ + true + end + end + + # Deprecated struct + class Retired + attr_reader :id + + def initialize(id:) + @id = id + end + + # Structural equality over every field. + def ==(other) + return false unless other.is_a?(Retired) + return false unless id == other.id + true + 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) + _wv_weight = r.read_f32 + Leaf.new(weight: _wv_weight) + end + + # @api private + # Packs a Inner into the value-buffer wire format. + def self._wv_write_inner(w, v) + _wv_write_leaf(w, v.leaf) + w.write_len(v.leaves.length) + v.leaves.each do |_wv_e0| + _wv_write_leaf(w, _wv_e0) + end + end + + # @api private + # Unpacks a Inner from the value-buffer wire format. + def self._wv_read_inner(r) + _wv_leaf = _wv_read_leaf(r) + _wv_leaves = Array.new(r.read_len) do + _wv_e0 = _wv_read_leaf(r) + _wv_e0 + end + Inner.new(leaf: _wv_leaf, leaves: _wv_leaves) + end + + # @api private + # Packs a Outer into the value-buffer wire format. + def self._wv_write_outer(w, v) + _wv_write_inner(w, v.inner) + if v.maybe_inner.nil? + w.write_flag(false) + else + w.write_flag(true) + _wv_write_inner(w, v.maybe_inner) + end + w.write_len(v.by_name.length) + v.by_name.each do |_wv_k0, _wv_v0| + w.write_string(_wv_k0) + _wv_write_inner(w, _wv_v0) + end + w.write_len(v.modes.length) + v.modes.each do |_wv_e0| + w.write_i32(_wv_e0) + end + w.write_len(v.matrix.length) + v.matrix.each do |_wv_e0| + w.write_len(_wv_e0.length) + _wv_e0.each do |_wv_e1| + w.write_i32(_wv_e1) + end + end + w.write_len(v.sparse.length) + v.sparse.each do |_wv_e0| + if _wv_e0.nil? + w.write_flag(false) + else + w.write_flag(true) + w.write_i64(_wv_e0) + end + end + if v.blob.nil? + w.write_flag(false) + else + w.write_flag(true) + w.write_bytes(v.blob) + end + if v.note.nil? + w.write_flag(false) + else + w.write_flag(true) + w.write_string(v.note) + end + w.write_len(v.by_id.length) + v.by_id.each do |_wv_k0, _wv_v0| + w.write_i64(_wv_k0) + w.write_string(_wv_v0) + end + w.write_bool(v._hidden) + w.write_u8(v.v2) + end + + # @api private + # Unpacks a Outer from the value-buffer wire format. + def self._wv_read_outer(r) + _wv_inner = _wv_read_inner(r) + if r.read_flag + _wv_maybe_inner = _wv_read_inner(r) + else + _wv_maybe_inner = nil + end + _wv_by_name = {} + r.read_len.times do + _wv_k0 = r.read_string + _wv_v0 = _wv_read_inner(r) + _wv_by_name[_wv_k0] = _wv_v0 + end + _wv_modes = Array.new(r.read_len) do + _wv_e0 = r.read_i32 + _wv_e0 + end + _wv_matrix = Array.new(r.read_len) do + _wv_e0 = Array.new(r.read_len) do + _wv_e1 = r.read_i32 + _wv_e1 + end + _wv_e0 + end + _wv_sparse = Array.new(r.read_len) do + if r.read_flag + _wv_e0 = r.read_i64 + else + _wv_e0 = nil + end + _wv_e0 + end + if r.read_flag + _wv_blob = r.read_bytes + else + _wv_blob = nil + end + if r.read_flag + _wv_note = r.read_string + else + _wv_note = nil + end + _wv_by_id = {} + r.read_len.times do + _wv_k0 = r.read_i64 + _wv_v0 = r.read_string + _wv_by_id[_wv_k0] = _wv_v0 + end + _wv__hidden = r.read_bool + _wv_v2 = r.read_u8 + Outer.new(inner: _wv_inner, maybe_inner: _wv_maybe_inner, by_name: _wv_by_name, modes: _wv_modes, matrix: _wv_matrix, sparse: _wv_sparse, blob: _wv_blob, note: _wv_note, by_id: _wv_by_id, _hidden: _wv__hidden, v2: _wv_v2) + 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) + _wv_class_ = r.read_i32 + _wv_def_ = r.read_i32 + _wv_import = r.read_i32 + _wv_from = r.read_i32 + _wv_func = r.read_i32 + _wv_range = r.read_i32 + _wv_register = r.read_i32 + _wv_volatile = r.read_i32 + _wv_new = r.read_i32 + _wv_default = r.read_i32 + _wv_end_ = r.read_i32 + _wv_in_ = r.read_i32 + _wv_is = r.read_i32 + _wv_int = r.read_i32 + _wv_var = r.read_i32 + _wv_val = r.read_i32 + _wv_self_ = r.read_i32 + _wv_this = r.read_i32 + _wv_object = r.read_i32 + _wv_string = r.read_i32 + _wv_case_ = r.read_i32 + _wv_select = r.read_i32 + _wv_switch = r.read_i32 + _wv_void = r.read_i32 + _wv_union = r.read_i32 + _wv_namespace = r.read_i32 + _wv_operator = r.read_i32 + _wv_template = r.read_i32 + _wv_package = r.read_i32 + _wv_protocol = r.read_i32 + _wv_guard = r.read_i32 + _wv_defer = r.read_i32 + _wv_inout = r.read_i32 + _wv_internal = r.read_i32 + _wv_when_ = r.read_i32 + _wv_fun = r.read_i32 + _wv_data = r.read_i32 + _wv_late = r.read_i32 + _wv_part = r.read_i32 + _wv_out = r.read_i32 + _wv_ref = r.read_i32 + _wv_event = r.read_i32 + _wv_base = r.read_i32 + _wv_lambda = r.read_i32 + _wv_yield_ = r.read_i32 + _wv_nil_ = r.read_i32 + _wv_null = r.read_i32 + _wv_begin_ = r.read_i32 + _wv_rescue_ = r.read_i32 + _wv_module_ = r.read_i32 + _wv_chan = r.read_i32 + _wv_go = r.read_i32 + _wv_let = r.read_i32 + _wv_export = r.read_i32 + _wv_delete = r.read_i32 + _wv_typedef = r.read_i32 + _wv_sizeof = r.read_i32 + _wv_static = r.read_i32 + _wv_super_ = r.read_i32 + Keywords.new(class_: _wv_class_, def_: _wv_def_, import: _wv_import, from: _wv_from, func: _wv_func, range: _wv_range, register: _wv_register, volatile: _wv_volatile, new: _wv_new, default: _wv_default, end_: _wv_end_, in_: _wv_in_, is: _wv_is, int: _wv_int, var: _wv_var, val: _wv_val, self_: _wv_self_, this: _wv_this, object: _wv_object, string: _wv_string, case_: _wv_case_, select: _wv_select, switch: _wv_switch, void: _wv_void, union: _wv_union, namespace: _wv_namespace, operator: _wv_operator, template: _wv_template, package: _wv_package, protocol: _wv_protocol, guard: _wv_guard, defer: _wv_defer, inout: _wv_inout, internal: _wv_internal, when_: _wv_when_, fun: _wv_fun, data: _wv_data, late: _wv_late, part: _wv_part, out: _wv_out, ref: _wv_ref, event: _wv_event, base: _wv_base, lambda: _wv_lambda, yield_: _wv_yield_, nil_: _wv_nil_, null: _wv_null, begin_: _wv_begin_, rescue_: _wv_rescue_, module_: _wv_module_, chan: _wv_chan, go: _wv_go, let: _wv_let, export: _wv_export, delete: _wv_delete, typedef: _wv_typedef, sizeof: _wv_sizeof, static: _wv_static, super_: _wv_super_) + 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) + _wv_id = r.read_i64 + Retired.new(id: _wv_id) + end + + attach_function :weaveffi_edge_cases_Widget_destroy, [:pointer], :void + # Void method that can fail + attach_function :weaveffi_edge_cases_Widget_refresh, [:pointer, :pointer], :void + attach_function :weaveffi_edge_cases_Widget_rename, [:pointer, :string, :pointer, :size_t, :pointer], :pointer + # Async method with a struct result + callback :weaveffi_edge_cases_Widget_fetch_callback, [:pointer, :pointer, :pointer, :size_t], :void + attach_function :weaveffi_edge_cases_Widget_fetch_async, [:pointer, :int64, :weaveffi_edge_cases_Widget_fetch_callback, :pointer], :void + attach_function :weaveffi_edge_cases_Widget_old_name, [:pointer, :pointer], :pointer + + attach_function :weaveffi_edge_cases_Vault_destroy, [:pointer], :void + # Fails when the vault is busy + attach_function :weaveffi_edge_cases_Vault_open, [:string, :pointer], :pointer + attach_function :weaveffi_edge_cases_Vault_peek, [:pointer, :pointer], :pointer + # Throwing static + attach_function :weaveffi_edge_cases_Vault_probe, [:pointer], :int32 + # Callback carrying composite payloads + callback :weaveffi_edge_cases_OnBatch_fn, [:pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :int32, :pointer], :void + callback :weaveffi_edge_cases_OnTick_fn, [:pointer], :void + attach_function :weaveffi_edge_cases_register_batch_listener, [:weaveffi_edge_cases_OnBatch_fn, :pointer], :uint64 + attach_function :weaveffi_edge_cases_unregister_batch_listener, [:uint64], :void + attach_function :weaveffi_edge_cases_register_tick_listener, [:weaveffi_edge_cases_OnTick_fn, :pointer], :uint64 + attach_function :weaveffi_edge_cases_unregister_tick_listener, [:uint64], :void + # No parameters and no result + attach_function :weaveffi_edge_cases_noop, [:pointer], :void + # Function named after a keyword whose parameters are all keywords + attach_function :weaveffi_edge_cases_select, [:int32, :string, :pointer, :size_t, :pointer, :size_t, :int32, :pointer], :int32 + # Optional list of optional structs, the deepest legal nesting + attach_function :weaveffi_edge_cases_nested_optional, [:pointer, :size_t, :pointer, :pointer], :pointer + attach_function :weaveffi_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 + # Write-back parameters + attach_function :weaveffi_edge_cases_mutable_params, [:string, :pointer, :size_t, :pointer], :void + # Factory for an interface with no constructor + attach_function :weaveffi_edge_cases_make_widget, [:string, :pointer], :pointer + # Optional interface return + attach_function :weaveffi_edge_cases_find_widget, [:int64, :pointer], :pointer + # Interface parameter, including an optional one + attach_function :weaveffi_edge_cases_inspect_widget, [:pointer, :pointer, :pointer], :pointer + attach_function :weaveffi_edge_cases_typed_handle_param, [:pointer, :uint64, :pointer], :pointer + # Iterator of strings + attach_function :weaveffi_edge_cases_string_stream, [:pointer], :pointer + attach_function :weaveffi_edge_cases_StringStreamIterator_next, [:pointer, :pointer, :pointer], :int32 + attach_function :weaveffi_edge_cases_StringStreamIterator_destroy, [:pointer], :void + # Iterator of scalars + attach_function :weaveffi_edge_cases_scalar_stream, [:pointer], :pointer + attach_function :weaveffi_edge_cases_ScalarStreamIterator_next, [:pointer, :pointer, :pointer], :int32 + attach_function :weaveffi_edge_cases_ScalarStreamIterator_destroy, [:pointer], :void + # Iterator of enums + attach_function :weaveffi_edge_cases_mode_stream, [:pointer], :pointer + attach_function :weaveffi_edge_cases_ModeStreamIterator_next, [:pointer, :pointer, :pointer], :int32 + attach_function :weaveffi_edge_cases_ModeStreamIterator_destroy, [:pointer], :void + # Async with no result + callback :weaveffi_edge_cases_async_void_callback, [:pointer, :pointer], :void + attach_function :weaveffi_edge_cases_async_void_async, [:weaveffi_edge_cases_async_void_callback, :pointer], :void + callback :weaveffi_edge_cases_async_bytes_callback, [:pointer, :pointer, :pointer, :size_t], :void + attach_function :weaveffi_edge_cases_async_bytes_async, [:uint32, :weaveffi_edge_cases_async_bytes_callback, :pointer], :void + callback :weaveffi_edge_cases_async_struct_callback, [:pointer, :pointer, :pointer, :size_t], :void + attach_function :weaveffi_edge_cases_async_struct_async, [:weaveffi_edge_cases_async_struct_callback, :pointer], :void + callback :weaveffi_edge_cases_async_list_callback, [:pointer, :pointer, :pointer, :size_t], :void + attach_function :weaveffi_edge_cases_async_list_async, [:pointer, :weaveffi_edge_cases_async_list_callback, :pointer], :void + callback :weaveffi_edge_cases_async_optional_callback, [:pointer, :pointer, :pointer, :size_t], :void + attach_function :weaveffi_edge_cases_async_optional_async, [:weaveffi_edge_cases_async_optional_callback, :pointer], :void + callback :weaveffi_edge_cases_async_enum_callback, [:pointer, :pointer, :int32], :void + attach_function :weaveffi_edge_cases_async_enum_async, [:weaveffi_edge_cases_async_enum_callback, :pointer], :void + callback :weaveffi_edge_cases_async_keywords_callback, [:pointer, :pointer, :pointer], :void + attach_function :weaveffi_edge_cases_async_keywords_async, [:string, :int32, :weaveffi_edge_cases_async_keywords_callback, :pointer], :void + attach_function :weaveffi_edge_cases_keywords_roundtrip, [:pointer, :size_t, :pointer, :pointer], :pointer + attach_function :weaveffi_edge_cases_legacy_value, [:int32, :pointer, :pointer], :pointer + + class WidgetPtr < FFI::AutoPointer + def self.release(ptr) + WeaveFFI.weaveffi_edge_cases_Widget_destroy(ptr) + end + end + + # Interface with no constructor; instances come from make_widget + class Widget + attr_reader :handle + + # Wraps an owned pointer the producer handed over, without + # re-running initialize. + def self._from_ptr(ptr) + obj = allocate + obj.instance_variable_set(:@handle, WidgetPtr.new(ptr)) + obj + end + + def destroy + return if @handle.nil? + @handle.free + @handle = nil + end + + # Void method that can fail + def refresh() + err = ErrorStruct.new + WeaveFFI.weaveffi_edge_cases_Widget_refresh(@handle, err) + WeaveFFI.check_edge_errors_error!(err) + end + + def rename(class_, import) + err = ErrorStruct.new + import_w = WvBufferWriter.new + if import.nil? + import_w.write_flag(false) + else + import_w.write_flag(true) + import_w.write_string(import) + end + import_data = import_w.data + import_buf = FFI::MemoryPointer.new(:uint8, import_data.bytesize) + import_buf.put_bytes(0, import_data) + result = WeaveFFI.weaveffi_edge_cases_Widget_rename(@handle, class_, import_buf, import_data.bytesize, err) + WeaveFFI.check_error!(err) + return '' if result.null? + str = result.read_string + WeaveFFI.weaveffi_free_string(result) + str + end + + # Async method with a struct result + # Blocks the current thread until the async producer completes; the + # result (or error) is delivered through the completion callback. + def fetch(id) + queue = Queue.new + callback = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |_context, err_ptr, result_ptr, result_len| + err = err_ptr.null? ? nil : ErrorStruct.new(err_ptr) + if err && err[:code] != 0 + code = err[:code] + msg = err[:message].null? ? '' : err[:message].read_string + payload = err[:payload_ptr].null? ? nil : err[:payload_ptr].read_string(err[:payload_len]) + WeaveFFI.weaveffi_error_free(err_ptr) + queue << WeaveFFI.edge_errors_error_from(code, msg, payload) + else + begin + _wv_r = WvBufferReader.new(result_ptr.null? ? ''.b : result_ptr.read_string(result_len)) + WeaveFFI.weaveffi_free_bytes(result_ptr, result_len) unless result_ptr.null? + _wv_v = WeaveFFI._wv_read_outer(_wv_r) + _wv_r.expect_end! + queue << _wv_v + rescue Error => e + queue << e + end + end + end + WeaveFFI.weaveffi_edge_cases_Widget_fetch_async(@handle, id, callback, FFI::Pointer::NULL) + value = queue.pop + raise value if value.is_a?(Error) + value + end + + def old_name() + warn "[DEPRECATED] Use rename instead" + err = ErrorStruct.new + result = WeaveFFI.weaveffi_edge_cases_Widget_old_name(@handle, err) + WeaveFFI.check_error!(err) + return '' if result.null? + str = result.read_string + WeaveFFI.weaveffi_free_string(result) + str + end + end + + class VaultPtr < FFI::AutoPointer + def self.release(ptr) + WeaveFFI.weaveffi_edge_cases_Vault_destroy(ptr) + end + end + + # Deprecated interface with a throwing constructor + class Vault + attr_reader :handle + + # Wraps an owned pointer the producer handed over, without + # re-running initialize. + def self._from_ptr(ptr) + obj = allocate + obj.instance_variable_set(:@handle, VaultPtr.new(ptr)) + obj + end + + def destroy + return if @handle.nil? + @handle.free + @handle = nil + end + + # Fails when the vault is busy + def self.open(path) + err = ErrorStruct.new + result = WeaveFFI.weaveffi_edge_cases_Vault_open(path, err) + WeaveFFI.check_edge_errors_error!(err) + raise Error.new(-1, 'null pointer') if result.null? + _from_ptr(result) + end + + def peek() + err = ErrorStruct.new + result = WeaveFFI.weaveffi_edge_cases_Vault_peek(@handle, err) + WeaveFFI.check_error!(err) + return nil if result.null? + Widget._from_ptr(result) + end + + # Throwing static + def self.probe() + err = ErrorStruct.new + result = WeaveFFI.weaveffi_edge_cases_Vault_probe(err) + WeaveFFI.check_edge_errors_error!(err) + result != 0 + end + end + + # Registers a OnBatch listener block. Returns a subscription id for + # unregister_batch_listener. + def self.register_batch_listener(&block) + trampoline = FFI::Function.new(:void, [:pointer, :size_t, :pointer, :size_t, :pointer, :size_t, :int32, :pointer]) do |items_ptr, items_len, raw_ptr, raw_len, first_ptr, first_len, mode, _context| + begin + items_r = WvBufferReader.new(items_ptr.null? ? ''.b : items_ptr.read_string(items_len)) + items_v = Array.new(items_r.read_len) do + _wv_e0 = _wv_read_outer(items_r) + _wv_e0 + end + items_r.expect_end! + first_r = WvBufferReader.new(first_ptr.null? ? ''.b : first_ptr.read_string(first_len)) + if first_r.read_flag + first_v = _wv_read_outer(first_r) + else + first_v = nil + end + first_r.expect_end! + rescue Error => e + warn "weaveffi: dropped OnBatch event: #{e.message}" + next + end + block.call(items_v, (raw_ptr.null? ? ''.b : raw_ptr.read_string(raw_len)), first_v, mode) + end + listener_id = weaveffi_edge_cases_register_batch_listener(trampoline, FFI::Pointer::NULL) + @listener_refs[listener_id] = trampoline + listener_id + end + + # Unregisters a listener previously registered with register_batch_listener. + def self.unregister_batch_listener(listener_id) + weaveffi_edge_cases_unregister_batch_listener(listener_id) + @listener_refs.delete(listener_id) + nil + end + + # Registers a OnTick listener block. Returns a subscription id for + # unregister_tick_listener. + def self.register_tick_listener(&block) + trampoline = FFI::Function.new(:void, [:pointer]) do |_context| + block.call() + end + listener_id = weaveffi_edge_cases_register_tick_listener(trampoline, FFI::Pointer::NULL) + @listener_refs[listener_id] = trampoline + listener_id + end + + # Unregisters a listener previously registered with register_tick_listener. + def self.unregister_tick_listener(listener_id) + weaveffi_edge_cases_unregister_tick_listener(listener_id) + @listener_refs.delete(listener_id) + nil + end + + # No parameters and no result + def self.noop() + err = ErrorStruct.new + weaveffi_edge_cases_noop(err) + check_error!(err) + end + + # Function named after a keyword whose parameters are all keywords + def self.select(class_, from, func, range, type) + err = ErrorStruct.new + func_w = WvBufferWriter.new + func_w.write_len(func.length) + func.each do |_wv_e0| + func_w.write_string(_wv_e0) + end + func_data = func_w.data + func_buf = FFI::MemoryPointer.new(:uint8, func_data.bytesize) + func_buf.put_bytes(0, func_data) + range_w = WvBufferWriter.new + if range.nil? + range_w.write_flag(false) + else + range_w.write_flag(true) + range_w.write_i32(range) + end + range_data = range_w.data + range_buf = FFI::MemoryPointer.new(:uint8, range_data.bytesize) + range_buf.put_bytes(0, range_data) + type_c = type ? 1 : 0 + result = weaveffi_edge_cases_select(class_, from, func_buf, func_data.bytesize, range_buf, range_data.bytesize, type_c, err) + check_error!(err) + result + end + + # Optional list of optional structs, the deepest legal nesting + def self.nested_optional(items) + err = ErrorStruct.new + items_w = WvBufferWriter.new + if items.nil? + items_w.write_flag(false) + else + items_w.write_flag(true) + items_w.write_len(items.length) + items.each do |_wv_e0| + if _wv_e0.nil? + items_w.write_flag(false) + else + items_w.write_flag(true) + _wv_write_outer(items_w, _wv_e0) + end + end + end + items_data = items_w.data + items_buf = FFI::MemoryPointer.new(:uint8, items_data.bytesize) + items_buf.put_bytes(0, items_data) + out_len = FFI::MemoryPointer.new(:size_t) + result = weaveffi_edge_cases_nested_optional(items_buf, items_data.bytesize, out_len, err) + check_error!(err) + len = out_len.read(:size_t) + data = result.null? ? ''.b : result.read_string(len) + weaveffi_free_bytes(result, len) unless result.null? + _wv_r = WvBufferReader.new(data) + if _wv_r.read_flag + _wv_value = Array.new(_wv_r.read_len) do + if _wv_r.read_flag + _wv_e0 = _wv_read_outer(_wv_r) + else + _wv_e0 = nil + end + _wv_e0 + end + else + _wv_value = nil + end + _wv_r.expect_end! + _wv_value + end + + def self.optional_params(text, count, outer, tags, blob, mode) + err = ErrorStruct.new + text_w = WvBufferWriter.new + if text.nil? + text_w.write_flag(false) + else + text_w.write_flag(true) + text_w.write_string(text) + end + text_data = text_w.data + text_buf = FFI::MemoryPointer.new(:uint8, text_data.bytesize) + text_buf.put_bytes(0, text_data) + count_w = WvBufferWriter.new + if count.nil? + count_w.write_flag(false) + else + count_w.write_flag(true) + count_w.write_i32(count) + end + count_data = count_w.data + count_buf = FFI::MemoryPointer.new(:uint8, count_data.bytesize) + count_buf.put_bytes(0, count_data) + outer_w = WvBufferWriter.new + if outer.nil? + outer_w.write_flag(false) + else + outer_w.write_flag(true) + _wv_write_outer(outer_w, outer) + end + outer_data = outer_w.data + outer_buf = FFI::MemoryPointer.new(:uint8, outer_data.bytesize) + outer_buf.put_bytes(0, outer_data) + tags_w = WvBufferWriter.new + if tags.nil? + tags_w.write_flag(false) + else + tags_w.write_flag(true) + tags_w.write_len(tags.length) + tags.each do |_wv_e0| + tags_w.write_string(_wv_e0) + end + end + tags_data = tags_w.data + tags_buf = FFI::MemoryPointer.new(:uint8, tags_data.bytesize) + tags_buf.put_bytes(0, tags_data) + blob_w = WvBufferWriter.new + if blob.nil? + blob_w.write_flag(false) + else + blob_w.write_flag(true) + blob_w.write_bytes(blob) + end + blob_data = blob_w.data + blob_buf = FFI::MemoryPointer.new(:uint8, blob_data.bytesize) + blob_buf.put_bytes(0, blob_data) + mode_w = WvBufferWriter.new + if mode.nil? + mode_w.write_flag(false) + else + mode_w.write_flag(true) + mode_w.write_i32(mode) + end + mode_data = mode_w.data + mode_buf = FFI::MemoryPointer.new(:uint8, mode_data.bytesize) + mode_buf.put_bytes(0, mode_data) + out_len = FFI::MemoryPointer.new(:size_t) + result = weaveffi_edge_cases_optional_params(text_buf, text_data.bytesize, count_buf, count_data.bytesize, outer_buf, outer_data.bytesize, tags_buf, tags_data.bytesize, blob_buf, blob_data.bytesize, mode_buf, mode_data.bytesize, out_len, err) + check_error!(err) + len = out_len.read(:size_t) + data = result.null? ? ''.b : result.read_string(len) + weaveffi_free_bytes(result, len) unless result.null? + _wv_r = WvBufferReader.new(data) + if _wv_r.read_flag + _wv_value = _wv_r.read_i32 + else + _wv_value = nil + end + _wv_r.expect_end! + _wv_value + end + + # Write-back parameters + def self.mutable_params(text, blob) + err = ErrorStruct.new + blob_buf = FFI::MemoryPointer.new(:uint8, blob.bytesize) + blob_buf.put_bytes(0, blob) + weaveffi_edge_cases_mutable_params(text, blob_buf, blob.bytesize, err) + check_error!(err) + end + + # Factory for an interface with no constructor + def self.make_widget(label) + err = ErrorStruct.new + result = weaveffi_edge_cases_make_widget(label, err) + check_error!(err) + raise Error.new(-1, 'null pointer') if result.null? + Widget._from_ptr(result) + end + + # Optional interface return + def self.find_widget(id) + err = ErrorStruct.new + result = weaveffi_edge_cases_find_widget(id, err) + check_edge_errors_error!(err) + return nil if result.null? + Widget._from_ptr(result) + end + + # Interface parameter, including an optional one + def self.inspect_widget(widget, fallback) + err = ErrorStruct.new + result = weaveffi_edge_cases_inspect_widget(widget.handle, fallback&.handle, err) + check_error!(err) + return '' if result.null? + str = result.read_string + weaveffi_free_string(result) + str + end + + def self.typed_handle_param(token, raw) + err = ErrorStruct.new + result = weaveffi_edge_cases_typed_handle_param(token.handle, raw, err) + check_error!(err) + raise Error.new(-1, 'null pointer') if result.null? + Leaf.new(result) + end + + # Iterator of strings + # Returns a lazy Enumerator that streams one element per pull; call + # `.to_a` to collect eagerly. The underlying producer iterator is + # launched on the first pull, so launch errors raise at that point + # rather than when this method returns. The iterator handle is + # released exactly once, when iteration finishes or is abandoned + # early (for example by `break`). + def self.string_stream() + Enumerator.new do |y| + err = ErrorStruct.new + iter = weaveffi_edge_cases_string_stream(err) + begin + check_error!(err) + unless iter.null? + loop do + out_item = FFI::MemoryPointer.new(:pointer) + item_err = ErrorStruct.new + has_item = weaveffi_edge_cases_StringStreamIterator_next(iter, out_item, item_err) + check_error!(item_err) + break if has_item.zero? + item_ptr = out_item.read_pointer + if item_ptr.null? + y << '' + else + item = item_ptr.read_string + weaveffi_free_string(item_ptr) + y << item + end + end + end + ensure + weaveffi_edge_cases_StringStreamIterator_destroy(iter) unless iter.null? + end + end + end + + # Iterator of scalars + # Returns a lazy Enumerator that streams one element per pull; call + # `.to_a` to collect eagerly. The underlying producer iterator is + # launched on the first pull, so launch errors raise at that point + # rather than when this method returns. The iterator handle is + # released exactly once, when iteration finishes or is abandoned + # early (for example by `break`). + def self.scalar_stream() + Enumerator.new do |y| + err = ErrorStruct.new + iter = weaveffi_edge_cases_scalar_stream(err) + begin + check_error!(err) + unless iter.null? + loop do + out_item = FFI::MemoryPointer.new(:int32) + item_err = ErrorStruct.new + has_item = weaveffi_edge_cases_ScalarStreamIterator_next(iter, out_item, item_err) + check_error!(item_err) + break if has_item.zero? + y << out_item.read_int32 + end + end + ensure + weaveffi_edge_cases_ScalarStreamIterator_destroy(iter) unless iter.null? + end + end + end + + # Iterator of enums + # Returns a lazy Enumerator that streams one element per pull; call + # `.to_a` to collect eagerly. The underlying producer iterator is + # launched on the first pull, so launch errors raise at that point + # rather than when this method returns. The iterator handle is + # released exactly once, when iteration finishes or is abandoned + # early (for example by `break`). + def self.mode_stream() + Enumerator.new do |y| + err = ErrorStruct.new + iter = weaveffi_edge_cases_mode_stream(err) + begin + check_error!(err) + unless iter.null? + loop do + out_item = FFI::MemoryPointer.new(:int32) + item_err = ErrorStruct.new + has_item = weaveffi_edge_cases_ModeStreamIterator_next(iter, out_item, item_err) + check_error!(item_err) + break if has_item.zero? + y << out_item.read_int32 + end + end + ensure + weaveffi_edge_cases_ModeStreamIterator_destroy(iter) unless iter.null? + end + end + end + + # Async with no result + # Blocks the current thread until the async producer completes; the + # result (or error) is delivered through the completion callback. + def self.async_void() + queue = Queue.new + callback = FFI::Function.new(:void, [:pointer, :pointer]) do |_context, err_ptr| + err = err_ptr.null? ? nil : ErrorStruct.new(err_ptr) + if err && err[:code] != 0 + code = err[:code] + msg = err[:message].null? ? '' : err[:message].read_string + weaveffi_error_free(err_ptr) + queue << Error.new(code, msg) + else + queue << nil + end + end + weaveffi_edge_cases_async_void_async(callback, FFI::Pointer::NULL) + value = queue.pop + raise value if value.is_a?(Error) + value + end + + # Blocks the current thread until the async producer completes; the + # result (or error) is delivered through the completion callback. + def self.async_bytes(size) + queue = Queue.new + callback = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |_context, err_ptr, result, result_len| + err = err_ptr.null? ? nil : ErrorStruct.new(err_ptr) + if err && err[:code] != 0 + code = err[:code] + msg = err[:message].null? ? '' : err[:message].read_string + weaveffi_error_free(err_ptr) + queue << Error.new(code, msg) + else + _wv_b = result.null? ? ''.b : result.read_string(result_len) + weaveffi_free_bytes(result, result_len) unless result.null? + queue << _wv_b + end + end + weaveffi_edge_cases_async_bytes_async(size, callback, FFI::Pointer::NULL) + value = queue.pop + raise value if value.is_a?(Error) + value + end + + # Blocks the current thread until the async producer completes; the + # result (or error) is delivered through the completion callback. + def self.async_struct() + queue = Queue.new + callback = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |_context, err_ptr, result_ptr, result_len| + err = err_ptr.null? ? nil : ErrorStruct.new(err_ptr) + if err && err[:code] != 0 + code = err[:code] + msg = err[:message].null? ? '' : err[:message].read_string + payload = err[:payload_ptr].null? ? nil : err[:payload_ptr].read_string(err[:payload_len]) + weaveffi_error_free(err_ptr) + queue << edge_errors_error_from(code, msg, payload) + else + begin + _wv_r = WvBufferReader.new(result_ptr.null? ? ''.b : result_ptr.read_string(result_len)) + weaveffi_free_bytes(result_ptr, result_len) unless result_ptr.null? + _wv_v = _wv_read_outer(_wv_r) + _wv_r.expect_end! + queue << _wv_v + rescue Error => e + queue << e + end + end + end + weaveffi_edge_cases_async_struct_async(callback, FFI::Pointer::NULL) + value = queue.pop + raise value if value.is_a?(Error) + value + end + + # Blocks the current thread until the async producer completes; the + # result (or error) is delivered through the completion callback (cancellation token not exposed; pass-through is NULL). + def self.async_list() + queue = Queue.new + callback = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |_context, err_ptr, result_ptr, result_len| + err = err_ptr.null? ? nil : ErrorStruct.new(err_ptr) + if err && err[:code] != 0 + code = err[:code] + msg = err[:message].null? ? '' : err[:message].read_string + payload = err[:payload_ptr].null? ? nil : err[:payload_ptr].read_string(err[:payload_len]) + weaveffi_error_free(err_ptr) + queue << edge_errors_error_from(code, msg, payload) + else + begin + _wv_r = WvBufferReader.new(result_ptr.null? ? ''.b : result_ptr.read_string(result_len)) + weaveffi_free_bytes(result_ptr, result_len) unless result_ptr.null? + _wv_v = Array.new(_wv_r.read_len) do + _wv_e0 = _wv_read_outer(_wv_r) + _wv_e0 + end + _wv_r.expect_end! + queue << _wv_v + rescue Error => e + queue << e + end + end + end + weaveffi_edge_cases_async_list_async(FFI::Pointer::NULL, callback, FFI::Pointer::NULL) + value = queue.pop + raise value if value.is_a?(Error) + value + end + + # Blocks the current thread until the async producer completes; the + # result (or error) is delivered through the completion callback. + def self.async_optional() + queue = Queue.new + callback = FFI::Function.new(:void, [:pointer, :pointer, :pointer, :size_t]) do |_context, err_ptr, result_ptr, result_len| + err = err_ptr.null? ? nil : ErrorStruct.new(err_ptr) + if err && err[:code] != 0 + code = err[:code] + msg = err[:message].null? ? '' : err[:message].read_string + weaveffi_error_free(err_ptr) + queue << Error.new(code, msg) + else + begin + _wv_r = WvBufferReader.new(result_ptr.null? ? ''.b : result_ptr.read_string(result_len)) + weaveffi_free_bytes(result_ptr, result_len) unless result_ptr.null? + if _wv_r.read_flag + _wv_v = _wv_r.read_i64 + else + _wv_v = nil + end + _wv_r.expect_end! + queue << _wv_v + rescue Error => e + queue << e + end + end + end + weaveffi_edge_cases_async_optional_async(callback, FFI::Pointer::NULL) + value = queue.pop + raise value if value.is_a?(Error) + value + end + + # Blocks the current thread until the async producer completes; the + # result (or error) is delivered through the completion callback. + def self.async_enum() + queue = Queue.new + callback = FFI::Function.new(:void, [:pointer, :pointer, :int32]) do |_context, err_ptr, result| + err = err_ptr.null? ? nil : ErrorStruct.new(err_ptr) + if err && err[:code] != 0 + code = err[:code] + msg = err[:message].null? ? '' : err[:message].read_string + weaveffi_error_free(err_ptr) + queue << Error.new(code, msg) + else + queue << result + end + end + weaveffi_edge_cases_async_enum_async(callback, FFI::Pointer::NULL) + value = queue.pop + raise value if value.is_a?(Error) + value + end + + # Blocks the current thread until the async producer completes; the + # result (or error) is delivered through the completion callback. + def self.async_keywords(class_, import) + queue = Queue.new + callback = FFI::Function.new(:void, [:pointer, :pointer, :pointer]) do |_context, err_ptr, result| + err = err_ptr.null? ? nil : ErrorStruct.new(err_ptr) + if err && err[:code] != 0 + code = err[:code] + msg = err[:message].null? ? '' : err[:message].read_string + weaveffi_error_free(err_ptr) + queue << Error.new(code, msg) + else + _wv_s = result.null? ? '' : result.read_string + weaveffi_free_string(result) unless result.null? + queue << _wv_s + end + end + weaveffi_edge_cases_async_keywords_async(class_, import, callback, FFI::Pointer::NULL) + value = queue.pop + raise value if value.is_a?(Error) + value + end + + def self.keywords_roundtrip(value) + err = ErrorStruct.new + value_w = WvBufferWriter.new + _wv_write_keywords(value_w, value) + value_data = value_w.data + value_buf = FFI::MemoryPointer.new(:uint8, value_data.bytesize) + value_buf.put_bytes(0, value_data) + out_len = FFI::MemoryPointer.new(:size_t) + result = weaveffi_edge_cases_keywords_roundtrip(value_buf, value_data.bytesize, out_len, err) + check_error!(err) + len = out_len.read(:size_t) + data = result.null? ? ''.b : result.read_string(len) + weaveffi_free_bytes(result, len) unless result.null? + _wv_r = WvBufferReader.new(data) + _wv_value = _wv_read_keywords(_wv_r) + _wv_r.expect_end! + _wv_value + end + + def self.legacy_value(value) + warn "[DEPRECATED] Use Mode-based APIs" + err = ErrorStruct.new + out_len = FFI::MemoryPointer.new(:size_t) + result = weaveffi_edge_cases_legacy_value(value, out_len, err) + check_error!(err) + len = out_len.read(:size_t) + data = result.null? ? ''.b : result.read_string(len) + weaveffi_free_bytes(result, len) unless result.null? + _wv_r = WvBufferReader.new(data) + _wv_value = _wv_read_retired(_wv_r) + _wv_r.expect_end! + _wv_value + end +end + +# END weaveffi.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_async_demo__weaveffi_gemspec.snap b/crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__weaveffi_gemspec.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/ruby_async_demo__weaveffi_gemspec.snap rename to crates/weaveffi-cli/tests/snapshots/ruby_edge_cases__weaveffi_gemspec.snap diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_events__README_md.snap b/crates/weaveffi-cli/tests/snapshots/ruby_events__README_md.snap deleted file mode 100644 index dfa27948..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_events__README_md.snap +++ /dev/null @@ -1,30 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# weaveffi (Ruby) - -Auto-generated Ruby bindings using the [ffi](https://github.com/ffi/ffi) gem. - -## Prerequisites - -- Ruby >= 2.7 -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path. - -## Install - -```bash -gem build weaveffi.gemspec -gem install weaveffi-0.1.0.gem -``` - -## Usage - -```ruby -require 'weaveffi' -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_events__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_events__lib_weaveffi_rb.snap deleted file mode 100644 index 8df2f71d..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_events__lib_weaveffi_rb.snap +++ /dev/null @@ -1,310 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -# frozen_string_literal: true -# WeaveFFI Ruby FFI bindings (auto-generated) - -require 'ffi' - -module WeaveFFI - extend FFI::Library - - # An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a - # specific build artifact regardless of its file name or location. - _wv_override = ENV['WEAVEFFI_LIBRARY'] - if _wv_override && !_wv_override.empty? - ffi_lib _wv_override - else - case FFI::Platform::OS - when /darwin/ - ffi_lib 'libweaveffi.dylib' - when /mswin|mingw/ - ffi_lib 'weaveffi.dll' - else - ffi_lib 'libweaveffi.so' - end - end - - class ErrorStruct < FFI::Struct - layout :code, :int32, - :message, :pointer, - :payload_ptr, :pointer, - :payload_len, :size_t - end - - class Error < StandardError - attr_reader :code - - def initialize(code, message) - @code = code - super(message) - end - end - - # @api private - # Appends values in the WeaveFFI value-buffer wire format: little-endian, - # packed, no alignment. - class WvBufferWriter - def initialize - @buf = +''.b - end - - # The encoded bytes as a binary String. - def data - @buf - end - - def write_bool(v) - @buf << (v ? "\x01".b : "\x00".b) - end - - def write_flag(v) - write_bool(v) - end - - def write_i8(v) - @buf << [v].pack('c') - end - - def write_u8(v) - @buf << [v].pack('C') - end - - def write_i16(v) - @buf << [v].pack('s<') - end - - def write_u16(v) - @buf << [v].pack('S<') - end - - def write_i32(v) - @buf << [v].pack('l<') - end - - def write_u32(v) - @buf << [v].pack('L<') - end - - def write_len(v) - write_u32(v) - end - - def write_i64(v) - @buf << [v].pack('q<') - end - - def write_u64(v) - @buf << [v].pack('Q<') - end - - def write_f32(v) - @buf << [v].pack('e') - end - - def write_f64(v) - @buf << [v].pack('E') - end - - def write_string(v) - b = v.to_s.encode(Encoding::UTF_8).b - write_u32(b.bytesize) - @buf << b - end - - def write_bytes(v) - b = v.to_s.b - write_u32(b.bytesize) - @buf << b - end - end - - # @api private - # Reads values in the WeaveFFI value-buffer wire format, raising Error on - # any malformed buffer. - class WvBufferReader - def initialize(data) - @data = data.to_s.b - @pos = 0 - end - - def take(n, what) - raise Error.new(-1, "malformed value buffer: #{what}") if @pos + n > @data.bytesize - s = @data.byteslice(@pos, n) - @pos += n - s - end - - def read_bool - b = take(1, 'bool').unpack1('C') - raise Error.new(-1, 'malformed value buffer: bool byte out of range') if b > 1 - b == 1 - end - - def read_flag - b = take(1, 'option flag').unpack1('C') - raise Error.new(-1, 'malformed value buffer: option flag out of range') if b > 1 - b == 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 - - def read_len - len = read_u32 - if len > @data.bytesize - @pos - raise Error.new(-1, 'malformed value buffer: length prefix exceeds remaining bytes') - end - len - 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 - s = take(read_len, 'string bytes').force_encoding(Encoding::UTF_8) - raise Error.new(-1, 'malformed value buffer: string is not valid UTF-8') unless s.valid_encoding? - s - end - - def read_bytes - take(read_len, 'byte buffer') - end - - def expect_end! - return if @pos == @data.bytesize - raise Error.new(-1, 'malformed value buffer: trailing bytes after value') - end - end - - attach_function :weaveffi_error_clear, [:pointer], :void - attach_function :weaveffi_error_free, [:pointer], :void - attach_function :weaveffi_free_string, [:pointer], :void - attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void - - def self.check_error!(err) - return if err[:code].zero? - code = err[:code] - msg_ptr = err[:message] - msg = msg_ptr.null? ? '' : msg_ptr.read_string - weaveffi_error_clear(err.to_ptr) - raise Error.new(code, msg) - end - - # Registered listener trampolines, keyed by subscription id. Holding the - # FFI::Function objects here keeps them alive until unregistered; without - # this the GC could collect a trampoline the producer still calls. - @listener_refs = {} - - # === Module: events === - callback :weaveffi_events_OnMessage_fn, [:string, :pointer], :void - attach_function :weaveffi_events_register_message_listener, [:weaveffi_events_OnMessage_fn, :pointer], :uint64 - attach_function :weaveffi_events_unregister_message_listener, [:uint64], :void - # Send a message, triggering the OnMessage callback - attach_function :weaveffi_events_send_message, [:string, :pointer], :void - # Return an iterator over all sent messages - attach_function :weaveffi_events_get_messages, [:pointer], :pointer - attach_function :weaveffi_events_GetMessagesIterator_next, [:pointer, :pointer, :pointer], :int32 - attach_function :weaveffi_events_GetMessagesIterator_destroy, [:pointer], :void - - # Registers a OnMessage listener block. Returns a subscription id for - # unregister_message_listener. - def self.register_message_listener(&block) - trampoline = FFI::Function.new(:void, [:string, :pointer]) do |message, _context| - block.call(message) - end - listener_id = weaveffi_events_register_message_listener(trampoline, FFI::Pointer::NULL) - @listener_refs[listener_id] = trampoline - listener_id - end - - # Unregisters a listener previously registered with register_message_listener. - def self.unregister_message_listener(listener_id) - weaveffi_events_unregister_message_listener(listener_id) - @listener_refs.delete(listener_id) - nil - end - - # Send a message, triggering the OnMessage callback - def self.send_message(text) - err = ErrorStruct.new - weaveffi_events_send_message(text, err) - check_error!(err) - end - - # Return an iterator over all sent messages - # Returns a lazy Enumerator that streams one element per pull; call - # `.to_a` to collect eagerly. The underlying producer iterator is - # launched on the first pull, so launch errors raise at that point - # rather than when this method returns. The iterator handle is - # released exactly once, when iteration finishes or is abandoned - # early (for example by `break`). - def self.get_messages() - Enumerator.new do |y| - err = ErrorStruct.new - iter = weaveffi_events_get_messages(err) - begin - check_error!(err) - unless iter.null? - loop do - out_item = FFI::MemoryPointer.new(:pointer) - item_err = ErrorStruct.new - has_item = weaveffi_events_GetMessagesIterator_next(iter, out_item, item_err) - check_error!(item_err) - break if has_item.zero? - item_ptr = out_item.read_pointer - if item_ptr.null? - y << '' - else - item = item_ptr.read_string - weaveffi_free_string(item_ptr) - y << item - end - end - end - ensure - weaveffi_events_GetMessagesIterator_destroy(iter) unless iter.null? - end - end - end -end - -# END weaveffi.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_events__weaveffi_gemspec.snap b/crates/weaveffi-cli/tests/snapshots/ruby_events__weaveffi_gemspec.snap deleted file mode 100644 index c132abf7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_events__weaveffi_gemspec.snap +++ /dev/null @@ -1,18 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -Gem::Specification.new do |s| - s.name = 'weaveffi' - s.version = '0.1.0' - s.summary = 'weaveffi bindings generated by WeaveFFI' - s.files = Dir['lib/**/*.rb'] - s.require_paths = ['lib'] - - s.add_dependency 'ffi', '~> 1.15' -end - -# END weaveffi.gemspec diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_inventory__README_md.snap b/crates/weaveffi-cli/tests/snapshots/ruby_inventory__README_md.snap deleted file mode 100644 index dfa27948..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_inventory__README_md.snap +++ /dev/null @@ -1,30 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# weaveffi (Ruby) - -Auto-generated Ruby bindings using the [ffi](https://github.com/ffi/ffi) gem. - -## Prerequisites - -- Ruby >= 2.7 -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path. - -## Install - -```bash -gem build weaveffi.gemspec -gem install weaveffi-0.1.0.gem -``` - -## Usage - -```ruby -require 'weaveffi' -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_inventory__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_inventory__lib_weaveffi_rb.snap deleted file mode 100644 index 6a467392..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_inventory__lib_weaveffi_rb.snap +++ /dev/null @@ -1,657 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -# frozen_string_literal: true -# WeaveFFI Ruby FFI bindings (auto-generated) - -require 'ffi' - -module WeaveFFI - extend FFI::Library - - # An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a - # specific build artifact regardless of its file name or location. - _wv_override = ENV['WEAVEFFI_LIBRARY'] - if _wv_override && !_wv_override.empty? - ffi_lib _wv_override - else - case FFI::Platform::OS - when /darwin/ - ffi_lib 'libweaveffi.dylib' - when /mswin|mingw/ - ffi_lib 'weaveffi.dll' - else - ffi_lib 'libweaveffi.so' - end - end - - class ErrorStruct < FFI::Struct - layout :code, :int32, - :message, :pointer, - :payload_ptr, :pointer, - :payload_len, :size_t - end - - class Error < StandardError - attr_reader :code - - def initialize(code, message) - @code = code - super(message) - end - end - - # @api private - # Appends values in the WeaveFFI value-buffer wire format: little-endian, - # packed, no alignment. - class WvBufferWriter - def initialize - @buf = +''.b - end - - # The encoded bytes as a binary String. - def data - @buf - end - - def write_bool(v) - @buf << (v ? "\x01".b : "\x00".b) - end - - def write_flag(v) - write_bool(v) - end - - def write_i8(v) - @buf << [v].pack('c') - end - - def write_u8(v) - @buf << [v].pack('C') - end - - def write_i16(v) - @buf << [v].pack('s<') - end - - def write_u16(v) - @buf << [v].pack('S<') - end - - def write_i32(v) - @buf << [v].pack('l<') - end - - def write_u32(v) - @buf << [v].pack('L<') - end - - def write_len(v) - write_u32(v) - end - - def write_i64(v) - @buf << [v].pack('q<') - end - - def write_u64(v) - @buf << [v].pack('Q<') - end - - def write_f32(v) - @buf << [v].pack('e') - end - - def write_f64(v) - @buf << [v].pack('E') - end - - def write_string(v) - b = v.to_s.encode(Encoding::UTF_8).b - write_u32(b.bytesize) - @buf << b - end - - def write_bytes(v) - b = v.to_s.b - write_u32(b.bytesize) - @buf << b - end - end - - # @api private - # Reads values in the WeaveFFI value-buffer wire format, raising Error on - # any malformed buffer. - class WvBufferReader - def initialize(data) - @data = data.to_s.b - @pos = 0 - end - - def take(n, what) - raise Error.new(-1, "malformed value buffer: #{what}") if @pos + n > @data.bytesize - s = @data.byteslice(@pos, n) - @pos += n - s - end - - def read_bool - b = take(1, 'bool').unpack1('C') - raise Error.new(-1, 'malformed value buffer: bool byte out of range') if b > 1 - b == 1 - end - - def read_flag - b = take(1, 'option flag').unpack1('C') - raise Error.new(-1, 'malformed value buffer: option flag out of range') if b > 1 - b == 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 - - def read_len - len = read_u32 - if len > @data.bytesize - @pos - raise Error.new(-1, 'malformed value buffer: length prefix exceeds remaining bytes') - end - len - 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 - s = take(read_len, 'string bytes').force_encoding(Encoding::UTF_8) - raise Error.new(-1, 'malformed value buffer: string is not valid UTF-8') unless s.valid_encoding? - s - end - - def read_bytes - take(read_len, 'byte buffer') - end - - def expect_end! - return if @pos == @data.bytesize - raise Error.new(-1, 'malformed value buffer: trailing bytes after value') - end - end - - attach_function :weaveffi_error_clear, [:pointer], :void - attach_function :weaveffi_error_free, [:pointer], :void - attach_function :weaveffi_free_string, [:pointer], :void - attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void - - def self.check_error!(err) - return if err[:code].zero? - code = err[:code] - msg_ptr = err[:message] - msg = msg_ptr.null? ? '' : msg_ptr.read_string - weaveffi_error_clear(err.to_ptr) - raise Error.new(code, msg) - end - - # === Module: products === - - # Base error for the `products` module's error domain. - class ProductsError < Error - # price must be positive - class InvalidPrice < ProductsError - CODE = 1 - - def initialize(message = 'price must be positive') - super(1, message) - end - end - - # product not found - class ProductNotFound < ProductsError - CODE = 2 - - def initialize(message = 'product not found') - super(2, message) - end - end - end - - # Maps each ABI code of the ProductsError domain to its error class. - PRODUCTS_ERROR_CODES = { - 1 => ProductsError::InvalidPrice, - 2 => ProductsError::ProductNotFound, - }.freeze - - # Builds the ProductsError subclass matching `code`, decoding any payload - # fields the code declares, or a generic Error for codes outside - # the domain (panics, marshalling). - def self.products_error_from(code, message, payload = nil) - cls = PRODUCTS_ERROR_CODES[code] - return Error.new(code, message) if cls.nil? - message.empty? ? cls.new : cls.new(message) - end - - # Raises the typed ProductsError for a non-zero error slot. - def self.check_products_error!(err) - return if err[:code].zero? - code = err[:code] - msg_ptr = err[:message] - msg = msg_ptr.null? ? '' : msg_ptr.read_string - payload_ptr = err[:payload_ptr] - payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) - weaveffi_error_clear(err.to_ptr) - raise products_error_from(code, msg, payload) - end - - module Category - ELECTRONICS = 0 - CLOTHING = 1 - FOOD = 2 - BOOKS = 3 - end - - class Product - attr_reader :id - - attr_reader :name - - attr_reader :description - - attr_reader :price - - attr_reader :category - - attr_reader :tags - - def initialize(id:, name:, description:, price:, category:, tags:) - @id = id - @name = name - @description = description - @price = price - @category = category - @tags = tags - end - - # Structural equality over every field. - def ==(other) - return false unless other.is_a?(Product) - return false unless id == other.id - return false unless name == other.name - return false unless description == other.description - return false unless price == other.price - return false unless category == other.category - return false unless tags == other.tags - true - end - end - - # @api private - # Packs a Product into the value-buffer wire format. - def self._wv_write_product(w, v) - w.write_i64(v.id) - w.write_string(v.name) - if v.description.nil? - w.write_flag(false) - else - w.write_flag(true) - w.write_string(v.description) - end - w.write_f64(v.price) - w.write_i32(v.category) - w.write_len(v.tags.length) - v.tags.each do |_wv_e0| - w.write_string(_wv_e0) - end - end - - # @api private - # Unpacks a Product from the value-buffer wire format. - def self._wv_read_product(r) - _wv_id = r.read_i64 - _wv_name = r.read_string - if r.read_flag - _wv_description = r.read_string - else - _wv_description = nil - end - _wv_price = r.read_f64 - _wv_category = r.read_i32 - _wv_tags = Array.new(r.read_len) do - _wv_e0 = r.read_string - _wv_e0 - end - Product.new(id: _wv_id, name: _wv_name, description: _wv_description, price: _wv_price, category: _wv_category, tags: _wv_tags) - end - - attach_function :weaveffi_products_Catalog_destroy, [:pointer], :void - attach_function :weaveffi_products_Catalog_new, [:pointer], :pointer - attach_function :weaveffi_products_Catalog_add_product, [:pointer, :string, :double, :int32, :pointer, :pointer], :pointer - attach_function :weaveffi_products_Catalog_get_product, [:pointer, :int64, :pointer, :pointer], :pointer - attach_function :weaveffi_products_Catalog_search, [:pointer, :int32, :pointer, :pointer], :pointer - attach_function :weaveffi_products_Catalog_update_price, [:pointer, :int64, :double, :pointer], :int32 - attach_function :weaveffi_products_Catalog_remove, [:pointer, :int64, :pointer], :int32 - - class CatalogPtr < FFI::AutoPointer - def self.release(ptr) - WeaveFFI.weaveffi_products_Catalog_destroy(ptr) - end - end - - # A product catalog owning its products - class Catalog - attr_reader :handle - - # Wraps an owned pointer the producer handed over, without - # re-running initialize. - def self._from_ptr(ptr) - obj = allocate - obj.instance_variable_set(:@handle, CatalogPtr.new(ptr)) - obj - end - - def destroy - return if @handle.nil? - @handle.free - @handle = nil - end - - def initialize() - err = ErrorStruct.new - result = WeaveFFI.weaveffi_products_Catalog_new(err) - WeaveFFI.check_error!(err) - raise Error.new(-1, 'null pointer') if result.null? - @handle = CatalogPtr.new(result) - end - - def add_product(name, price, category) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = WeaveFFI.weaveffi_products_Catalog_add_product(@handle, name, price, category, out_len, err) - WeaveFFI.check_products_error!(err) - len = out_len.read(:size_t) - data = result.null? ? ''.b : result.read_string(len) - WeaveFFI.weaveffi_free_bytes(result, len) unless result.null? - _wv_r = WvBufferReader.new(data) - _wv_value = WeaveFFI._wv_read_product(_wv_r) - _wv_r.expect_end! - _wv_value - end - - def get_product(id) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = WeaveFFI.weaveffi_products_Catalog_get_product(@handle, id, out_len, err) - WeaveFFI.check_products_error!(err) - len = out_len.read(:size_t) - data = result.null? ? ''.b : result.read_string(len) - WeaveFFI.weaveffi_free_bytes(result, len) unless result.null? - _wv_r = WvBufferReader.new(data) - _wv_value = WeaveFFI._wv_read_product(_wv_r) - _wv_r.expect_end! - _wv_value - end - - def search(category) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = WeaveFFI.weaveffi_products_Catalog_search(@handle, category, out_len, err) - WeaveFFI.check_error!(err) - len = out_len.read(:size_t) - data = result.null? ? ''.b : result.read_string(len) - WeaveFFI.weaveffi_free_bytes(result, len) unless result.null? - _wv_r = WvBufferReader.new(data) - _wv_value = Array.new(_wv_r.read_len) do - _wv_e0 = WeaveFFI._wv_read_product(_wv_r) - _wv_e0 - end - _wv_r.expect_end! - _wv_value - end - - def update_price(id, price) - err = ErrorStruct.new - result = WeaveFFI.weaveffi_products_Catalog_update_price(@handle, id, price, err) - WeaveFFI.check_products_error!(err) - result != 0 - end - - def remove(id) - err = ErrorStruct.new - result = WeaveFFI.weaveffi_products_Catalog_remove(@handle, id, err) - WeaveFFI.check_error!(err) - result != 0 - end - end - - # === Module: orders === - - # Base error for the `orders` module's error domain. - class OrdersError < Error - # order not found - class OrderNotFound < OrdersError - CODE = 1 - - def initialize(message = 'order not found') - super(1, message) - end - end - - # order must contain at least one item - class EmptyOrder < OrdersError - CODE = 2 - - def initialize(message = 'order must contain at least one item') - super(2, message) - end - end - end - - # Maps each ABI code of the OrdersError domain to its error class. - ORDERS_ERROR_CODES = { - 1 => OrdersError::OrderNotFound, - 2 => OrdersError::EmptyOrder, - }.freeze - - # Builds the OrdersError subclass matching `code`, decoding any payload - # fields the code declares, or a generic Error for codes outside - # the domain (panics, marshalling). - def self.orders_error_from(code, message, payload = nil) - cls = ORDERS_ERROR_CODES[code] - return Error.new(code, message) if cls.nil? - message.empty? ? cls.new : cls.new(message) - end - - # Raises the typed OrdersError for a non-zero error slot. - def self.check_orders_error!(err) - return if err[:code].zero? - code = err[:code] - msg_ptr = err[:message] - msg = msg_ptr.null? ? '' : msg_ptr.read_string - payload_ptr = err[:payload_ptr] - payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) - weaveffi_error_clear(err.to_ptr) - raise orders_error_from(code, msg, payload) - end - - class OrderItem - attr_reader :product_id - - attr_reader :quantity - - attr_reader :unit_price - - def initialize(product_id:, quantity:, unit_price:) - @product_id = product_id - @quantity = quantity - @unit_price = unit_price - end - - # Structural equality over every field. - def ==(other) - return false unless other.is_a?(OrderItem) - return false unless product_id == other.product_id - return false unless quantity == other.quantity - return false unless unit_price == other.unit_price - true - end - end - - class Order - attr_reader :id - - attr_reader :items - - attr_reader :total - - attr_reader :status - - def initialize(id:, items:, total:, status:) - @id = id - @items = items - @total = total - @status = status - end - - # Structural equality over every field. - def ==(other) - return false unless other.is_a?(Order) - return false unless id == other.id - return false unless items == other.items - return false unless total == other.total - return false unless status == other.status - true - end - end - - # @api private - # Packs a OrderItem into the value-buffer wire format. - def self._wv_write_order_item(w, v) - w.write_i64(v.product_id) - w.write_i32(v.quantity) - w.write_f64(v.unit_price) - end - - # @api private - # Unpacks a OrderItem from the value-buffer wire format. - def self._wv_read_order_item(r) - _wv_product_id = r.read_i64 - _wv_quantity = r.read_i32 - _wv_unit_price = r.read_f64 - OrderItem.new(product_id: _wv_product_id, quantity: _wv_quantity, unit_price: _wv_unit_price) - end - - # @api private - # Packs a Order into the value-buffer wire format. - def self._wv_write_order(w, v) - w.write_i64(v.id) - w.write_len(v.items.length) - v.items.each do |_wv_e0| - _wv_write_order_item(w, _wv_e0) - end - w.write_f64(v.total) - w.write_string(v.status) - end - - # @api private - # Unpacks a Order from the value-buffer wire format. - def self._wv_read_order(r) - _wv_id = r.read_i64 - _wv_items = Array.new(r.read_len) do - _wv_e0 = _wv_read_order_item(r) - _wv_e0 - end - _wv_total = r.read_f64 - _wv_status = r.read_string - Order.new(id: _wv_id, items: _wv_items, total: _wv_total, status: _wv_status) - end - attach_function :weaveffi_orders_create_order, [:pointer, :size_t, :pointer], :int64 - attach_function :weaveffi_orders_get_order, [:int64, :pointer, :pointer], :pointer - attach_function :weaveffi_orders_cancel_order, [:int64, :pointer], :int32 - attach_function :weaveffi_orders_add_product_to_order, [:int64, :pointer, :size_t, :pointer], :int32 - - def self.create_order(items) - err = ErrorStruct.new - items_w = WvBufferWriter.new - items_w.write_len(items.length) - items.each do |_wv_e0| - _wv_write_order_item(items_w, _wv_e0) - end - items_data = items_w.data - items_buf = FFI::MemoryPointer.new(:uint8, items_data.bytesize) - items_buf.put_bytes(0, items_data) - result = weaveffi_orders_create_order(items_buf, items_data.bytesize, err) - check_orders_error!(err) - result - end - - def self.get_order(id) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_orders_get_order(id, out_len, err) - check_orders_error!(err) - len = out_len.read(:size_t) - data = result.null? ? ''.b : result.read_string(len) - weaveffi_free_bytes(result, len) unless result.null? - _wv_r = WvBufferReader.new(data) - _wv_value = _wv_read_order(_wv_r) - _wv_r.expect_end! - _wv_value - end - - def self.cancel_order(id) - err = ErrorStruct.new - result = weaveffi_orders_cancel_order(id, err) - check_error!(err) - result != 0 - end - - def self.add_product_to_order(order_id, product) - err = ErrorStruct.new - product_w = WvBufferWriter.new - _wv_write_product(product_w, product) - product_data = product_w.data - product_buf = FFI::MemoryPointer.new(:uint8, product_data.bytesize) - product_buf.put_bytes(0, product_data) - result = weaveffi_orders_add_product_to_order(order_id, product_buf, product_data.bytesize, err) - check_error!(err) - result != 0 - end -end - -# END weaveffi.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_inventory__weaveffi_gemspec.snap b/crates/weaveffi-cli/tests/snapshots/ruby_inventory__weaveffi_gemspec.snap deleted file mode 100644 index c132abf7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_inventory__weaveffi_gemspec.snap +++ /dev/null @@ -1,18 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -Gem::Specification.new do |s| - s.name = 'weaveffi' - s.version = '0.1.0' - s.summary = 'weaveffi bindings generated by WeaveFFI' - s.files = Dir['lib/**/*.rb'] - s.require_paths = ['lib'] - - s.add_dependency 'ffi', '~> 1.15' -end - -# END weaveffi.gemspec diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_weaveffi_rb.snap index e99049ea..5830a08d 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_weaveffi_rb.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_kitchen_sink__lib_weaveffi_rb.snap @@ -29,6 +29,22 @@ module WeaveFFI end end + # The ABI revision these bindings were generated against. Checked before any + # other symbol is attached so a mismatched producer fails at require time + # instead of misreading the error struct or a value buffer later. + ABI_VERSION = 1 + begin + attach_function :weaveffi_abi_version, [], :uint32 + rescue FFI::NotFoundError + raise LoadError, 'the loaded WeaveFFI library predates ABI versioning ' \ + "(these bindings expect ABI revision #{ABI_VERSION})" + end + _wv_abi = weaveffi_abi_version + unless _wv_abi == ABI_VERSION + raise LoadError, "WeaveFFI ABI mismatch: these bindings expect revision #{ABI_VERSION} " \ + "but the loaded library reports revision #{_wv_abi}" + end + class ErrorStruct < FFI::Struct layout :code, :int32, :message, :pointer, diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_kvstore__README_md.snap b/crates/weaveffi-cli/tests/snapshots/ruby_kvstore__README_md.snap deleted file mode 100644 index dfa27948..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_kvstore__README_md.snap +++ /dev/null @@ -1,30 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# weaveffi (Ruby) - -Auto-generated Ruby bindings using the [ffi](https://github.com/ffi/ffi) gem. - -## Prerequisites - -- Ruby >= 2.7 -- The compiled shared library (`libweaveffi.so`, `libweaveffi.dylib`, or `weaveffi.dll`) available on your library search path. - -## Install - -```bash -gem build weaveffi.gemspec -gem install weaveffi-0.1.0.gem -``` - -## Usage - -```ruby -require 'weaveffi' -``` - - diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_kvstore__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_kvstore__lib_weaveffi_rb.snap deleted file mode 100644 index 3f1fa29f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_kvstore__lib_weaveffi_rb.snap +++ /dev/null @@ -1,709 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -# frozen_string_literal: true -# WeaveFFI Ruby FFI bindings (auto-generated) - -require 'ffi' - -module WeaveFFI - extend FFI::Library - - # An explicit path in WEAVEFFI_LIBRARY wins, so callers can point at a - # specific build artifact regardless of its file name or location. - _wv_override = ENV['WEAVEFFI_LIBRARY'] - if _wv_override && !_wv_override.empty? - ffi_lib _wv_override - else - case FFI::Platform::OS - when /darwin/ - ffi_lib 'libweaveffi.dylib' - when /mswin|mingw/ - ffi_lib 'weaveffi.dll' - else - ffi_lib 'libweaveffi.so' - end - end - - class ErrorStruct < FFI::Struct - layout :code, :int32, - :message, :pointer, - :payload_ptr, :pointer, - :payload_len, :size_t - end - - class Error < StandardError - attr_reader :code - - def initialize(code, message) - @code = code - super(message) - end - end - - # @api private - # Appends values in the WeaveFFI value-buffer wire format: little-endian, - # packed, no alignment. - class WvBufferWriter - def initialize - @buf = +''.b - end - - # The encoded bytes as a binary String. - def data - @buf - end - - def write_bool(v) - @buf << (v ? "\x01".b : "\x00".b) - end - - def write_flag(v) - write_bool(v) - end - - def write_i8(v) - @buf << [v].pack('c') - end - - def write_u8(v) - @buf << [v].pack('C') - end - - def write_i16(v) - @buf << [v].pack('s<') - end - - def write_u16(v) - @buf << [v].pack('S<') - end - - def write_i32(v) - @buf << [v].pack('l<') - end - - def write_u32(v) - @buf << [v].pack('L<') - end - - def write_len(v) - write_u32(v) - end - - def write_i64(v) - @buf << [v].pack('q<') - end - - def write_u64(v) - @buf << [v].pack('Q<') - end - - def write_f32(v) - @buf << [v].pack('e') - end - - def write_f64(v) - @buf << [v].pack('E') - end - - def write_string(v) - b = v.to_s.encode(Encoding::UTF_8).b - write_u32(b.bytesize) - @buf << b - end - - def write_bytes(v) - b = v.to_s.b - write_u32(b.bytesize) - @buf << b - end - end - - # @api private - # Reads values in the WeaveFFI value-buffer wire format, raising Error on - # any malformed buffer. - class WvBufferReader - def initialize(data) - @data = data.to_s.b - @pos = 0 - end - - def take(n, what) - raise Error.new(-1, "malformed value buffer: #{what}") if @pos + n > @data.bytesize - s = @data.byteslice(@pos, n) - @pos += n - s - end - - def read_bool - b = take(1, 'bool').unpack1('C') - raise Error.new(-1, 'malformed value buffer: bool byte out of range') if b > 1 - b == 1 - end - - def read_flag - b = take(1, 'option flag').unpack1('C') - raise Error.new(-1, 'malformed value buffer: option flag out of range') if b > 1 - b == 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 - - def read_len - len = read_u32 - if len > @data.bytesize - @pos - raise Error.new(-1, 'malformed value buffer: length prefix exceeds remaining bytes') - end - len - 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 - s = take(read_len, 'string bytes').force_encoding(Encoding::UTF_8) - raise Error.new(-1, 'malformed value buffer: string is not valid UTF-8') unless s.valid_encoding? - s - end - - def read_bytes - take(read_len, 'byte buffer') - end - - def expect_end! - return if @pos == @data.bytesize - raise Error.new(-1, 'malformed value buffer: trailing bytes after value') - end - end - - attach_function :weaveffi_error_clear, [:pointer], :void - attach_function :weaveffi_error_free, [:pointer], :void - attach_function :weaveffi_free_string, [:pointer], :void - attach_function :weaveffi_free_bytes, [:pointer, :size_t], :void - - def self.check_error!(err) - return if err[:code].zero? - code = err[:code] - msg_ptr = err[:message] - msg = msg_ptr.null? ? '' : msg_ptr.read_string - weaveffi_error_clear(err.to_ptr) - raise Error.new(code, msg) - end - - # Registered listener trampolines, keyed by subscription id. Holding the - # FFI::Function objects here keeps them alive until unregistered; without - # this the GC could collect a trampoline the producer still calls. - @listener_refs = {} - - # === Module: kv === - - # Base error for the `kv` module's error domain. - class KvError < Error - # No entry exists for the requested key - class KeyNotFound < KvError - CODE = 1001 - - def initialize(message = 'key not found') - super(1001, message) - end - end - - # The entry exists but its TTL has elapsed - class Expired < KvError - CODE = 1002 - - def initialize(message = 'entry expired') - super(1002, message) - end - end - - # The store has reached its capacity limit - class StoreFull < KvError - CODE = 1003 - - def initialize(message = 'store has reached capacity') - super(1003, message) - end - end - - # Underlying storage reported an I/O failure - class IoError < KvError - CODE = 1004 - - def initialize(message = 'I/O failure') - super(1004, message) - end - end - end - - # Maps each ABI code of the KvError domain to its error class. - KV_ERROR_CODES = { - 1001 => KvError::KeyNotFound, - 1002 => KvError::Expired, - 1003 => KvError::StoreFull, - 1004 => KvError::IoError, - }.freeze - - # Builds the KvError subclass matching `code`, decoding any payload - # fields the code declares, or a generic Error for codes outside - # the domain (panics, marshalling). - def self.kv_error_from(code, message, payload = nil) - cls = KV_ERROR_CODES[code] - return Error.new(code, message) if cls.nil? - message.empty? ? cls.new : cls.new(message) - end - - # Raises the typed KvError for a non-zero error slot. - def self.check_kv_error!(err) - return if err[:code].zero? - code = err[:code] - msg_ptr = err[:message] - msg = msg_ptr.null? ? '' : msg_ptr.read_string - payload_ptr = err[:payload_ptr] - payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len]) - weaveffi_error_clear(err.to_ptr) - raise kv_error_from(code, msg, payload) - end - - # Persistence semantics applied to a stored entry - module EntryKind - # In-memory only; lost on close - VOLATILE = 0 - # Flushed to durable storage - PERSISTENT = 1 - # Persistent and encrypted at rest - ENCRYPTED = 2 - end - - # A single key-value entry persisted in the store - class Entry - # Stable monotonic identifier assigned on insert - attr_reader :id - - # UTF-8 lookup key - attr_reader :key - - # Opaque binary payload - attr_reader :value - - # Unix-timestamp seconds when the entry was created - attr_reader :created_at - - # Optional unix-timestamp seconds at which the entry expires - attr_reader :expires_at - - # Free-form labels attached to the entry - attr_reader :tags - - # Arbitrary string-valued metadata pairs - attr_reader :metadata - - def initialize(id:, key:, value:, created_at:, expires_at:, tags:, metadata:) - @id = id - @key = key - @value = value - @created_at = created_at - @expires_at = expires_at - @tags = tags - @metadata = metadata - end - - # Structural equality over every field. - def ==(other) - return false unless other.is_a?(Entry) - return false unless id == other.id - return false unless key == other.key - return false unless value == other.value - return false unless created_at == other.created_at - return false unless expires_at == other.expires_at - return false unless tags == other.tags - return false unless metadata == other.metadata - true - end - end - - # @api private - # Packs a Entry into the value-buffer wire format. - def self._wv_write_entry(w, v) - w.write_i64(v.id) - w.write_string(v.key) - w.write_bytes(v.value) - w.write_i64(v.created_at) - if v.expires_at.nil? - w.write_flag(false) - else - w.write_flag(true) - w.write_i64(v.expires_at) - end - w.write_len(v.tags.length) - v.tags.each do |_wv_e0| - w.write_string(_wv_e0) - end - w.write_len(v.metadata.length) - v.metadata.each do |_wv_k0, _wv_v0| - w.write_string(_wv_k0) - w.write_string(_wv_v0) - end - end - - # @api private - # Unpacks a Entry from the value-buffer wire format. - def self._wv_read_entry(r) - _wv_id = r.read_i64 - _wv_key = r.read_string - _wv_value = r.read_bytes - _wv_created_at = r.read_i64 - if r.read_flag - _wv_expires_at = r.read_i64 - else - _wv_expires_at = nil - end - _wv_tags = Array.new(r.read_len) do - _wv_e0 = r.read_string - _wv_e0 - end - _wv_metadata = {} - r.read_len.times do - _wv_k0 = r.read_string - _wv_v0 = r.read_string - _wv_metadata[_wv_k0] = _wv_v0 - end - Entry.new(id: _wv_id, key: _wv_key, value: _wv_value, created_at: _wv_created_at, expires_at: _wv_expires_at, tags: _wv_tags, metadata: _wv_metadata) - end - - attach_function :weaveffi_kv_Store_destroy, [:pointer], :void - # Open (or create) a store backed by the given filesystem path - attach_function :weaveffi_kv_Store_open, [:string, :pointer], :pointer - # Insert or replace a value, returning true on success - attach_function :weaveffi_kv_Store_put, [:pointer, :string, :pointer, :size_t, :int32, :pointer, :size_t, :pointer], :int32 - # Look up an entry by key; returns null if missing or expired - attach_function :weaveffi_kv_Store_get, [:pointer, :string, :pointer, :pointer], :pointer - # Remove the entry for the given key, returning true if it existed - attach_function :weaveffi_kv_Store_delete, [:pointer, :string, :pointer], :int32 - # Stream every key, optionally filtered by a prefix - attach_function :weaveffi_kv_Store_list_keys, [:pointer, :pointer, :size_t, :pointer], :pointer - attach_function :weaveffi_kv_Store_ListKeysIterator_next, [:pointer, :pointer, :pointer], :int32 - attach_function :weaveffi_kv_Store_ListKeysIterator_destroy, [:pointer], :void - # Return the number of live entries in the store - attach_function :weaveffi_kv_Store_count, [:pointer, :pointer], :int64 - # Drop every entry from the store - attach_function :weaveffi_kv_Store_clear, [:pointer, :pointer], :void - # Reclaim space asynchronously; returns the number of bytes reclaimed - callback :weaveffi_kv_Store_compact_callback, [:pointer, :pointer, :int64], :void - attach_function :weaveffi_kv_Store_compact_async, [:pointer, :pointer, :weaveffi_kv_Store_compact_callback, :pointer], :void - # Legacy single-shot put kept for compatibility - attach_function :weaveffi_kv_Store_legacy_put, [:pointer, :string, :pointer, :size_t, :pointer], :int32 - # The largest number of live entries one store will hold - attach_function :weaveffi_kv_Store_default_capacity, [:pointer], :int64 - # Fires when an entry is evicted from the store - callback :weaveffi_kv_OnEvict_fn, [:string, :pointer], :void - attach_function :weaveffi_kv_register_eviction_listener, [:weaveffi_kv_OnEvict_fn, :pointer], :uint64 - attach_function :weaveffi_kv_unregister_eviction_listener, [:uint64], :void - - class StorePtr < FFI::AutoPointer - def self.release(ptr) - WeaveFFI.weaveffi_kv_Store_destroy(ptr) - end - end - - # An in-memory key-value store owning its entries; destroying the object releases all of its state - class Store - attr_reader :handle - - # Wraps an owned pointer the producer handed over, without - # re-running initialize. - def self._from_ptr(ptr) - obj = allocate - obj.instance_variable_set(:@handle, StorePtr.new(ptr)) - obj - end - - def destroy - return if @handle.nil? - @handle.free - @handle = nil - end - - # Open (or create) a store backed by the given filesystem path - def self.open(path) - err = ErrorStruct.new - result = WeaveFFI.weaveffi_kv_Store_open(path, err) - WeaveFFI.check_kv_error!(err) - raise Error.new(-1, 'null pointer') if result.null? - _from_ptr(result) - end - - # Insert or replace a value, returning true on success - def put(key, value, kind, ttl_seconds) - err = ErrorStruct.new - value_buf = FFI::MemoryPointer.new(:uint8, value.bytesize) - value_buf.put_bytes(0, value) - ttl_seconds_w = WvBufferWriter.new - if ttl_seconds.nil? - ttl_seconds_w.write_flag(false) - else - ttl_seconds_w.write_flag(true) - ttl_seconds_w.write_i64(ttl_seconds) - end - ttl_seconds_data = ttl_seconds_w.data - ttl_seconds_buf = FFI::MemoryPointer.new(:uint8, ttl_seconds_data.bytesize) - ttl_seconds_buf.put_bytes(0, ttl_seconds_data) - result = WeaveFFI.weaveffi_kv_Store_put(@handle, key, value_buf, value.bytesize, kind, ttl_seconds_buf, ttl_seconds_data.bytesize, err) - WeaveFFI.check_kv_error!(err) - result != 0 - end - - # Look up an entry by key; returns null if missing or expired - def get(key) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = WeaveFFI.weaveffi_kv_Store_get(@handle, key, out_len, err) - WeaveFFI.check_kv_error!(err) - len = out_len.read(:size_t) - data = result.null? ? ''.b : result.read_string(len) - WeaveFFI.weaveffi_free_bytes(result, len) unless result.null? - _wv_r = WvBufferReader.new(data) - if _wv_r.read_flag - _wv_value = WeaveFFI._wv_read_entry(_wv_r) - else - _wv_value = nil - end - _wv_r.expect_end! - _wv_value - end - - # Remove the entry for the given key, returning true if it existed - def delete(key) - err = ErrorStruct.new - result = WeaveFFI.weaveffi_kv_Store_delete(@handle, key, err) - WeaveFFI.check_kv_error!(err) - result != 0 - end - - # Stream every key, optionally filtered by a prefix - # Returns a lazy Enumerator that streams one element per pull; call - # `.to_a` to collect eagerly. The underlying producer iterator is - # launched on the first pull, so launch errors raise at that point - # rather than when this method returns. The iterator handle is - # released exactly once, when iteration finishes or is abandoned - # early (for example by `break`). - def list_keys(prefix) - prefix_w = WvBufferWriter.new - if prefix.nil? - prefix_w.write_flag(false) - else - prefix_w.write_flag(true) - prefix_w.write_string(prefix) - end - prefix_data = prefix_w.data - prefix_buf = FFI::MemoryPointer.new(:uint8, prefix_data.bytesize) - prefix_buf.put_bytes(0, prefix_data) - Enumerator.new do |y| - err = ErrorStruct.new - iter = WeaveFFI.weaveffi_kv_Store_list_keys(@handle, prefix_buf, prefix_data.bytesize, err) - begin - WeaveFFI.check_kv_error!(err) - unless iter.null? - loop do - out_item = FFI::MemoryPointer.new(:pointer) - item_err = ErrorStruct.new - has_item = WeaveFFI.weaveffi_kv_Store_ListKeysIterator_next(iter, out_item, item_err) - WeaveFFI.check_kv_error!(item_err) - break if has_item.zero? - item_ptr = out_item.read_pointer - if item_ptr.null? - y << '' - else - item = item_ptr.read_string - WeaveFFI.weaveffi_free_string(item_ptr) - y << item - end - end - end - ensure - WeaveFFI.weaveffi_kv_Store_ListKeysIterator_destroy(iter) unless iter.null? - end - end - end - - # Return the number of live entries in the store - def count() - err = ErrorStruct.new - result = WeaveFFI.weaveffi_kv_Store_count(@handle, err) - WeaveFFI.check_error!(err) - result - end - - # Drop every entry from the store - def clear() - err = ErrorStruct.new - WeaveFFI.weaveffi_kv_Store_clear(@handle, err) - WeaveFFI.check_error!(err) - end - - # Reclaim space asynchronously; returns the number of bytes reclaimed - # Blocks the current thread until the async producer completes; the - # result (or error) is delivered through the completion callback (cancellation token not exposed; pass-through is NULL). - def compact() - queue = Queue.new - callback = FFI::Function.new(:void, [:pointer, :pointer, :int64]) do |_context, err_ptr, result| - err = err_ptr.null? ? nil : ErrorStruct.new(err_ptr) - if err && err[:code] != 0 - code = err[:code] - msg = err[:message].null? ? '' : err[:message].read_string - payload = err[:payload_ptr].null? ? nil : err[:payload_ptr].read_string(err[:payload_len]) - WeaveFFI.weaveffi_error_free(err_ptr) - queue << WeaveFFI.kv_error_from(code, msg, payload) - else - queue << result - end - end - WeaveFFI.weaveffi_kv_Store_compact_async(@handle, FFI::Pointer::NULL, callback, FFI::Pointer::NULL) - value = queue.pop - raise value if value.is_a?(Error) - value - end - - # Legacy single-shot put kept for compatibility - def legacy_put(key, value) - warn "[DEPRECATED] use put() with explicit kind" - err = ErrorStruct.new - value_buf = FFI::MemoryPointer.new(:uint8, value.bytesize) - value_buf.put_bytes(0, value) - result = WeaveFFI.weaveffi_kv_Store_legacy_put(@handle, key, value_buf, value.bytesize, err) - WeaveFFI.check_kv_error!(err) - result != 0 - end - - # The largest number of live entries one store will hold - def self.default_capacity() - err = ErrorStruct.new - result = WeaveFFI.weaveffi_kv_Store_default_capacity(err) - WeaveFFI.check_error!(err) - result - end - end - - # Subscribe to per-key eviction notifications - # Registers a OnEvict listener block. Returns a subscription id for - # unregister_eviction_listener. - def self.register_eviction_listener(&block) - trampoline = FFI::Function.new(:void, [:string, :pointer]) do |key, _context| - block.call(key) - end - listener_id = weaveffi_kv_register_eviction_listener(trampoline, FFI::Pointer::NULL) - @listener_refs[listener_id] = trampoline - listener_id - end - - # Unregisters a listener previously registered with register_eviction_listener. - def self.unregister_eviction_listener(listener_id) - weaveffi_kv_unregister_eviction_listener(listener_id) - @listener_refs.delete(listener_id) - nil - end - - # === Module: kv_stats === - - # Aggregate store statistics - class Stats - # Number of live entries - attr_reader :total_entries - - # Sum of all value byte lengths - attr_reader :total_bytes - - # Number of entries past their TTL but not yet evicted - attr_reader :expired_entries - - def initialize(total_entries:, total_bytes:, expired_entries:) - @total_entries = total_entries - @total_bytes = total_bytes - @expired_entries = expired_entries - end - - # Structural equality over every field. - def ==(other) - return false unless other.is_a?(Stats) - return false unless total_entries == other.total_entries - return false unless total_bytes == other.total_bytes - return false unless expired_entries == other.expired_entries - true - end - end - - # @api private - # Packs a Stats into the value-buffer wire format. - def self._wv_write_stats(w, v) - w.write_i64(v.total_entries) - w.write_i64(v.total_bytes) - w.write_i64(v.expired_entries) - end - - # @api private - # Unpacks a Stats from the value-buffer wire format. - def self._wv_read_stats(r) - _wv_total_entries = r.read_i64 - _wv_total_bytes = r.read_i64 - _wv_expired_entries = r.read_i64 - Stats.new(total_entries: _wv_total_entries, total_bytes: _wv_total_bytes, expired_entries: _wv_expired_entries) - end - # Snapshot the current store statistics - attach_function :weaveffi_kv_stats_get_stats, [:pointer, :pointer, :pointer], :pointer - - # Snapshot the current store statistics - def self.get_stats(store) - err = ErrorStruct.new - out_len = FFI::MemoryPointer.new(:size_t) - result = weaveffi_kv_stats_get_stats(store.handle, out_len, err) - check_kv_error!(err) - len = out_len.read(:size_t) - data = result.null? ? ''.b : result.read_string(len) - weaveffi_free_bytes(result, len) unless result.null? - _wv_r = WvBufferReader.new(data) - _wv_value = _wv_read_stats(_wv_r) - _wv_r.expect_end! - _wv_value - end -end - -# END weaveffi.rb diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_kvstore__weaveffi_gemspec.snap b/crates/weaveffi-cli/tests/snapshots/ruby_kvstore__weaveffi_gemspec.snap deleted file mode 100644 index c132abf7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/ruby_kvstore__weaveffi_gemspec.snap +++ /dev/null @@ -1,18 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -# Generated by WeaveFFI [VERSION] from weaveffi.yml -# DO NOT EDIT. Your changes will be overwritten. -# To regenerate: weaveffi generate weaveffi.yml -o - -Gem::Specification.new do |s| - s.name = 'weaveffi' - s.version = '0.1.0' - s.summary = 'weaveffi bindings generated by WeaveFFI' - s.files = Dir['lib/**/*.rb'] - s.require_paths = ['lib'] - - s.add_dependency 'ffi', '~> 1.15' -end - -# END weaveffi.gemspec diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_weaveffi_rb.snap index 92c6b9ea..7373796c 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_weaveffi_rb.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_nested_modules__lib_weaveffi_rb.snap @@ -29,6 +29,22 @@ module WeaveFFI end end + # The ABI revision these bindings were generated against. Checked before any + # other symbol is attached so a mismatched producer fails at require time + # instead of misreading the error struct or a value buffer later. + ABI_VERSION = 1 + begin + attach_function :weaveffi_abi_version, [], :uint32 + rescue FFI::NotFoundError + raise LoadError, 'the loaded WeaveFFI library predates ABI versioning ' \ + "(these bindings expect ABI revision #{ABI_VERSION})" + end + _wv_abi = weaveffi_abi_version + unless _wv_abi == ABI_VERSION + raise LoadError, "WeaveFFI ABI mismatch: these bindings expect revision #{ABI_VERSION} " \ + "but the loaded library reports revision #{_wv_abi}" + end + class ErrorStruct < FFI::Struct layout :code, :int32, :message, :pointer, diff --git a/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_weaveffi_rb.snap b/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_weaveffi_rb.snap index 2958d1fb..cc7e0094 100644 --- a/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_weaveffi_rb.snap +++ b/crates/weaveffi-cli/tests/snapshots/ruby_shapes__lib_weaveffi_rb.snap @@ -29,6 +29,22 @@ module WeaveFFI end end + # The ABI revision these bindings were generated against. Checked before any + # other symbol is attached so a mismatched producer fails at require time + # instead of misreading the error struct or a value buffer later. + ABI_VERSION = 1 + begin + attach_function :weaveffi_abi_version, [], :uint32 + rescue FFI::NotFoundError + raise LoadError, 'the loaded WeaveFFI library predates ABI versioning ' \ + "(these bindings expect ABI revision #{ABI_VERSION})" + end + _wv_abi = weaveffi_abi_version + unless _wv_abi == ABI_VERSION + raise LoadError, "WeaveFFI ABI mismatch: these bindings expect revision #{ABI_VERSION} " \ + "but the loaded library reports revision #{_wv_abi}" + end + class ErrorStruct < FFI::Struct layout :code, :int32, :message, :pointer, diff --git a/crates/weaveffi-cli/tests/snapshots/swift_async_demo__Sources_WeaveFFI_WeaveFFI_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_async_demo__Sources_WeaveFFI_WeaveFFI_swift.snap deleted file mode 100644 index a189616a..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_async_demo__Sources_WeaveFFI_WeaveFFI_swift.snap +++ /dev/null @@ -1,334 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -import CWeaveFFI -import Foundation - -public enum WeaveFFIError: Error, LocalizedError { - case error(code: Int32, message: String) - public var errorDescription: String? { - switch self { - case let .error(_, message): return message - } - } - public var errorCode: Int32 { - switch self { - case let .error(code, _): return code - } - } -} - -@inline(__always) -func check(_ err: inout weaveffi_error) throws { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - weaveffi_error_clear(&err) - throw WeaveFFIError.error(code: code, message: message) - } -} - -@inline(__always) -func trap(_ err: inout weaveffi_error) { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - weaveffi_error_clear(&err) - fatalError("\(code): \(message)") - } -} - -/// Serializes values into the WeaveFFI value-buffer wire format -/// (little-endian, packed, no alignment). -struct WvWriter { - var bytes: [UInt8] = [] - - mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } - mutating func writeInt8(_ v: Int8) { bytes.append(UInt8(bitPattern: v)) } - mutating func writeUInt8(_ v: UInt8) { bytes.append(v) } - mutating func writeUInt16(_ v: UInt16) { - bytes.append(UInt8(truncatingIfNeeded: v)) - bytes.append(UInt8(truncatingIfNeeded: v >> 8)) - } - mutating func writeInt16(_ v: Int16) { writeUInt16(UInt16(bitPattern: v)) } - mutating func writeUInt32(_ v: UInt32) { - bytes.append(UInt8(truncatingIfNeeded: v)) - bytes.append(UInt8(truncatingIfNeeded: v >> 8)) - bytes.append(UInt8(truncatingIfNeeded: v >> 16)) - bytes.append(UInt8(truncatingIfNeeded: v >> 24)) - } - mutating func writeInt32(_ v: Int32) { writeUInt32(UInt32(bitPattern: v)) } - mutating func writeUInt64(_ v: UInt64) { - writeUInt32(UInt32(truncatingIfNeeded: v)) - writeUInt32(UInt32(truncatingIfNeeded: v >> 32)) - } - mutating func writeInt64(_ v: Int64) { writeUInt64(UInt64(bitPattern: v)) } - mutating func writeFloat(_ v: Float) { writeUInt32(v.bitPattern) } - mutating func writeDouble(_ v: Double) { writeUInt64(v.bitPattern) } - mutating func writeLen(_ n: Int) { - precondition(n >= 0 && n <= Int(UInt32.max), "WeaveFFI buffer length exceeds UInt32.max") - writeUInt32(UInt32(n)) - } - mutating func writeString(_ v: String) { - let utf8 = Array(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) } -} - -/// Traps on a malformed value buffer. Per the wire-format spec, consumers -/// surface decode failures through the same channel as a producer panic. -func wvDecodeFailure(_ context: String) -> Never { - fatalError("malformed WeaveFFI value buffer: \(context)") -} - -/// Deserializes values from the WeaveFFI value-buffer wire format, rejecting -/// truncated buffers, invalid flag bytes, oversized length prefixes, and -/// trailing bytes. -struct WvReader { - let bytes: [UInt8] - var pos: Int = 0 - - init(bytes: [UInt8]) { self.bytes = bytes } - - var remaining: Int { bytes.count - pos } - - mutating func take(_ n: Int, _ context: String) -> ArraySlice { - guard remaining >= n else { wvDecodeFailure(context) } - defer { pos += n } - return bytes[pos..<(pos + n)] - } - - mutating func readBool() -> Bool { - switch take(1, "bool").first! { - case 0: return false - case 1: return true - default: wvDecodeFailure("bool byte out of range") - } - } - mutating func readUInt8() -> UInt8 { take(1, "u8").first! } - mutating func readInt8() -> Int8 { Int8(bitPattern: readUInt8()) } - mutating func readUInt16() -> UInt16 { - var v: UInt16 = 0 - for (i, b) in take(2, "u16").enumerated() { v |= UInt16(b) << (8 * i) } - return v - } - mutating func readInt16() -> Int16 { Int16(bitPattern: readUInt16()) } - mutating func readUInt32() -> UInt32 { - var v: UInt32 = 0 - for (i, b) in take(4, "u32").enumerated() { v |= UInt32(b) << (8 * i) } - return v - } - mutating func readInt32() -> Int32 { Int32(bitPattern: readUInt32()) } - mutating func readUInt64() -> UInt64 { - var v: UInt64 = 0 - for (i, b) in take(8, "u64").enumerated() { v |= UInt64(b) << (8 * i) } - return v - } - mutating func readInt64() -> Int64 { Int64(bitPattern: readUInt64()) } - mutating func readFloat() -> Float { Float(bitPattern: readUInt32()) } - mutating func readDouble() -> Double { Double(bitPattern: readUInt64()) } - mutating func readLen() -> Int { - let n = Int(readUInt32()) - guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } - return n - } - mutating func readString() -> String { - let n = readLen() - guard let s = String(bytes: take(n, "string bytes"), encoding: .utf8) else { - wvDecodeFailure("string is not valid UTF-8") - } - return s - } - mutating func readBytes() -> Data { - let n = readLen() - return Data(take(n, "byte buffer")) - } - mutating func readOptionFlag() -> Bool { - switch take(1, "option flag").first! { - case 0: return false - case 1: return true - default: wvDecodeFailure("option flag byte out of range") - } - } - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -private final class ContinuationRef { - let value: CheckedContinuation - init(_ value: CheckedContinuation) { self.value = value } -} - -/// Typed errors reported by the `tasks` module. -public enum TaskError: Error, LocalizedError { - case invalidName(message: String) - public var errorDescription: String? { - switch self { - case let .invalidName(message): return message - } - } - /// The numeric ABI code carried by this error. - public var errorCode: Int32 { - switch self { - case .invalidName: return 1 - } - } -} - -@inline(__always) -func mapTasks(code: Int32, message: String, payload: [UInt8]?) -> Error { - switch code { - case 1: return TaskError.invalidName(message: message.isEmpty ? "task name must not be empty" : message) - default: return WeaveFFIError.error(code: code, message: message) - } -} - -@inline(__always) -func checkTasks(_ err: inout weaveffi_error) throws { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - let payload: [UInt8]? = err.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.payload_len)) } - weaveffi_error_clear(&err) - throw mapTasks(code: code, message: message, payload: payload) - } -} - -public struct TaskResult { - public var id: Int64 - public var value: String - public var success: Bool - - /// Creates a `TaskResult` value. - public init(id: Int64, value: String, success: Bool) { - self.id = id - self.value = value - self.success = success - } -} - -/// Serializes a `TaskResult` into the value-buffer wire format. -func wvWriteTaskResult(_ value: TaskResult, into w: inout WvWriter) { - w.writeInt64(value.id) - w.writeString(value.value) - w.writeBool(value.success) -} - -/// Deserializes a `TaskResult` from the value-buffer wire format. -func wvReadTaskResult(_ r: inout WvReader) -> TaskResult { - let v0 = r.readInt64() - let v1 = r.readString() - let v2 = r.readBool() - return TaskResult(id: v0, value: v1, success: v2) -} - -public enum Tasks { - public static func runTask(name: String) async throws -> TaskResult { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque() - name.withCString { name_ptr in - weaveffi_tasks_run_task_async(name_ptr, { context, err, resultPtr, resultLen in - let contRef = Unmanaged>.fromOpaque(context!).takeRetainedValue() - if let err = err, err.pointee.code != 0 { - let code = err.pointee.code - let msg = err.pointee.message.flatMap { String(cString: $0) } ?? "" - let payload: [UInt8]? = err.pointee.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.pointee.payload_len)) } - weaveffi_error_free(err) - contRef.value.resume(throwing: mapTasks(code: code, message: msg, payload: payload)) - } else { - guard let resultPtr = resultPtr else { - contRef.value.resume(throwing: WeaveFFIError.error(code: -1, message: "null buffer")) - return - } - let resultBytes = [UInt8](UnsafeBufferPointer(start: resultPtr, count: resultLen)) - weaveffi_free_bytes(UnsafeMutablePointer(mutating: resultPtr), resultLen) - var resultReader = WvReader(bytes: resultBytes) - let v0 = wvReadTaskResult(&resultReader) - resultReader.finish() - contRef.value.resume(returning: v0) - } - }, ctx) - } - } - } - public static func runBatch(names: [String]) async -> [TaskResult] { - await withCheckedContinuation { (continuation: CheckedContinuation<[TaskResult], Never>) in - let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque() - var namesWriter = WvWriter() - namesWriter.writeLen(names.count) - for v0 in names { - namesWriter.writeString(v0) - } - namesWriter.bytes.withUnsafeBufferPointer { names_buf in - let names_ptr = names_buf.baseAddress - let names_len = names_buf.count - weaveffi_tasks_run_batch_async(names_ptr, names_len, { context, err, resultPtr, resultLen in - let contRef = Unmanaged>.fromOpaque(context!).takeRetainedValue() - if let err = err, err.pointee.code != 0 { - let code = err.pointee.code - let msg = err.pointee.message.flatMap { String(cString: $0) } ?? "" - weaveffi_error_free(err) - fatalError("\(code): \(msg)") - } else { - guard let resultPtr = resultPtr else { - fatalError("-1: null buffer") - } - let resultBytes = [UInt8](UnsafeBufferPointer(start: resultPtr, count: resultLen)) - weaveffi_free_bytes(UnsafeMutablePointer(mutating: resultPtr), resultLen) - var resultReader = WvReader(bytes: resultBytes) - let n2 = resultReader.readLen() - var v1: [TaskResult] = [] - v1.reserveCapacity(n2) - for _ in 0.. Bool { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_tasks_cancel_task(id, &err) - trap(&err) - return rv - } - public static func runNTasks(n: Int32) async -> Int32 { - await withCheckedContinuation { (continuation: CheckedContinuation) in - let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque() - weaveffi_tasks_run_n_tasks_async(n, { context, err, result in - let contRef = Unmanaged>.fromOpaque(context!).takeRetainedValue() - if let err = err, err.pointee.code != 0 { - let code = err.pointee.code - let msg = err.pointee.message.flatMap { String(cString: $0) } ?? "" - weaveffi_error_free(err) - fatalError("\(code): \(msg)") - } else { - contRef.value.resume(returning: result) - } - }, ctx) - } - } - public static func activeCallbacks() -> Int64 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_tasks_active_callbacks(&err) - trap(&err) - return rv - } -} - -// END WeaveFFI.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_calculator__Package_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_calculator__Package_swift.snap deleted file mode 100644 index 7ec6e100..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_calculator__Package_swift.snap +++ /dev/null @@ -1,23 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// swift-tools-version:5.7 -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -import PackageDescription - -let package = Package( - name: "WeaveFFI", - platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], - products: [ - .library(name: "WeaveFFI", targets: ["WeaveFFI"]), - ], - targets: [ - .systemLibrary(name: "CWeaveFFI"), - .target(name: "WeaveFFI", dependencies: ["CWeaveFFI"]), - ] -) - -// END Package.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_calculator__Sources_CWeaveFFI_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_calculator__Sources_CWeaveFFI_module_modulemap.snap deleted file mode 100644 index e97f96e9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_calculator__Sources_CWeaveFFI_module_modulemap.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -module CWeaveFFI [system] { - header "../../../c/weaveffi.h" - link "weaveffi" - export * -} - -// END module.modulemap diff --git a/crates/weaveffi-cli/tests/snapshots/swift_calculator__Sources_WeaveFFI_WeaveFFI_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_calculator__Sources_WeaveFFI_WeaveFFI_swift.snap deleted file mode 100644 index 5185f4a2..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_calculator__Sources_WeaveFFI_WeaveFFI_swift.snap +++ /dev/null @@ -1,238 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -import CWeaveFFI -import Foundation - -public enum WeaveFFIError: Error, LocalizedError { - case error(code: Int32, message: String) - public var errorDescription: String? { - switch self { - case let .error(_, message): return message - } - } - public var errorCode: Int32 { - switch self { - case let .error(code, _): return code - } - } -} - -@inline(__always) -func check(_ err: inout weaveffi_error) throws { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - weaveffi_error_clear(&err) - throw WeaveFFIError.error(code: code, message: message) - } -} - -@inline(__always) -func trap(_ err: inout weaveffi_error) { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - weaveffi_error_clear(&err) - fatalError("\(code): \(message)") - } -} - -/// Serializes values into the WeaveFFI value-buffer wire format -/// (little-endian, packed, no alignment). -struct WvWriter { - var bytes: [UInt8] = [] - - mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } - mutating func writeInt8(_ v: Int8) { bytes.append(UInt8(bitPattern: v)) } - mutating func writeUInt8(_ v: UInt8) { bytes.append(v) } - mutating func writeUInt16(_ v: UInt16) { - bytes.append(UInt8(truncatingIfNeeded: v)) - bytes.append(UInt8(truncatingIfNeeded: v >> 8)) - } - mutating func writeInt16(_ v: Int16) { writeUInt16(UInt16(bitPattern: v)) } - mutating func writeUInt32(_ v: UInt32) { - bytes.append(UInt8(truncatingIfNeeded: v)) - bytes.append(UInt8(truncatingIfNeeded: v >> 8)) - bytes.append(UInt8(truncatingIfNeeded: v >> 16)) - bytes.append(UInt8(truncatingIfNeeded: v >> 24)) - } - mutating func writeInt32(_ v: Int32) { writeUInt32(UInt32(bitPattern: v)) } - mutating func writeUInt64(_ v: UInt64) { - writeUInt32(UInt32(truncatingIfNeeded: v)) - writeUInt32(UInt32(truncatingIfNeeded: v >> 32)) - } - mutating func writeInt64(_ v: Int64) { writeUInt64(UInt64(bitPattern: v)) } - mutating func writeFloat(_ v: Float) { writeUInt32(v.bitPattern) } - mutating func writeDouble(_ v: Double) { writeUInt64(v.bitPattern) } - mutating func writeLen(_ n: Int) { - precondition(n >= 0 && n <= Int(UInt32.max), "WeaveFFI buffer length exceeds UInt32.max") - writeUInt32(UInt32(n)) - } - mutating func writeString(_ v: String) { - let utf8 = Array(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) } -} - -/// Traps on a malformed value buffer. Per the wire-format spec, consumers -/// surface decode failures through the same channel as a producer panic. -func wvDecodeFailure(_ context: String) -> Never { - fatalError("malformed WeaveFFI value buffer: \(context)") -} - -/// Deserializes values from the WeaveFFI value-buffer wire format, rejecting -/// truncated buffers, invalid flag bytes, oversized length prefixes, and -/// trailing bytes. -struct WvReader { - let bytes: [UInt8] - var pos: Int = 0 - - init(bytes: [UInt8]) { self.bytes = bytes } - - var remaining: Int { bytes.count - pos } - - mutating func take(_ n: Int, _ context: String) -> ArraySlice { - guard remaining >= n else { wvDecodeFailure(context) } - defer { pos += n } - return bytes[pos..<(pos + n)] - } - - mutating func readBool() -> Bool { - switch take(1, "bool").first! { - case 0: return false - case 1: return true - default: wvDecodeFailure("bool byte out of range") - } - } - mutating func readUInt8() -> UInt8 { take(1, "u8").first! } - mutating func readInt8() -> Int8 { Int8(bitPattern: readUInt8()) } - mutating func readUInt16() -> UInt16 { - var v: UInt16 = 0 - for (i, b) in take(2, "u16").enumerated() { v |= UInt16(b) << (8 * i) } - return v - } - mutating func readInt16() -> Int16 { Int16(bitPattern: readUInt16()) } - mutating func readUInt32() -> UInt32 { - var v: UInt32 = 0 - for (i, b) in take(4, "u32").enumerated() { v |= UInt32(b) << (8 * i) } - return v - } - mutating func readInt32() -> Int32 { Int32(bitPattern: readUInt32()) } - mutating func readUInt64() -> UInt64 { - var v: UInt64 = 0 - for (i, b) in take(8, "u64").enumerated() { v |= UInt64(b) << (8 * i) } - return v - } - mutating func readInt64() -> Int64 { Int64(bitPattern: readUInt64()) } - mutating func readFloat() -> Float { Float(bitPattern: readUInt32()) } - mutating func readDouble() -> Double { Double(bitPattern: readUInt64()) } - mutating func readLen() -> Int { - let n = Int(readUInt32()) - guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } - return n - } - mutating func readString() -> String { - let n = readLen() - guard let s = String(bytes: take(n, "string bytes"), encoding: .utf8) else { - wvDecodeFailure("string is not valid UTF-8") - } - return s - } - mutating func readBytes() -> Data { - let n = readLen() - return Data(take(n, "byte buffer")) - } - mutating func readOptionFlag() -> Bool { - switch take(1, "option flag").first! { - case 0: return false - case 1: return true - default: wvDecodeFailure("option flag byte out of range") - } - } - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -/// Typed errors reported by the `calculator` module. -public enum CalcError: Error, LocalizedError { - case divisionByZero(message: String) - public var errorDescription: String? { - switch self { - case let .divisionByZero(message): return message - } - } - /// The numeric ABI code carried by this error. - public var errorCode: Int32 { - switch self { - case .divisionByZero: return 1 - } - } -} - -@inline(__always) -func mapCalculator(code: Int32, message: String, payload: [UInt8]?) -> Error { - switch code { - case 1: return CalcError.divisionByZero(message: message.isEmpty ? "division by zero" : message) - default: return WeaveFFIError.error(code: code, message: message) - } -} - -@inline(__always) -func checkCalculator(_ err: inout weaveffi_error) throws { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - let payload: [UInt8]? = err.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.payload_len)) } - weaveffi_error_clear(&err) - throw mapCalculator(code: code, message: message, payload: payload) - } -} - -public enum Calculator { - /// Add two integers - public static func add(a: Int32, b: Int32) -> Int32 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_calculator_add(a, b, &err) - trap(&err) - return rv - } - /// Multiply two integers - public static func mul(a: Int32, b: Int32) -> Int32 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_calculator_mul(a, b, &err) - trap(&err) - return rv - } - /// Divide two integers, failing on a zero divisor - public static func div(a: Int32, b: Int32) throws -> Int32 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_calculator_div(a, b, &err) - try checkCalculator(&err) - return rv - } - /// Echo a string back - public static func echo(s: String) -> String { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv: UnsafePointer? = s.withCString { s_ptr in - return weaveffi_calculator_echo(s_ptr, &err) - } - trap(&err) - guard let rv = rv else { fatalError("-1: null string") } - defer { weaveffi_free_string(rv) } - return String(cString: rv) - } -} - -// END WeaveFFI.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_contacts__Package_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_contacts__Package_swift.snap deleted file mode 100644 index 7ec6e100..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_contacts__Package_swift.snap +++ /dev/null @@ -1,23 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// swift-tools-version:5.7 -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -import PackageDescription - -let package = Package( - name: "WeaveFFI", - platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], - products: [ - .library(name: "WeaveFFI", targets: ["WeaveFFI"]), - ], - targets: [ - .systemLibrary(name: "CWeaveFFI"), - .target(name: "WeaveFFI", dependencies: ["CWeaveFFI"]), - ] -) - -// END Package.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_contacts__Sources_CWeaveFFI_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_contacts__Sources_CWeaveFFI_module_modulemap.snap deleted file mode 100644 index e97f96e9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_contacts__Sources_CWeaveFFI_module_modulemap.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -module CWeaveFFI [system] { - header "../../../c/weaveffi.h" - link "weaveffi" - export * -} - -// END module.modulemap diff --git a/crates/weaveffi-cli/tests/snapshots/swift_contacts__Sources_WeaveFFI_WeaveFFI_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_contacts__Sources_WeaveFFI_WeaveFFI_swift.snap deleted file mode 100644 index 20b811fd..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_contacts__Sources_WeaveFFI_WeaveFFI_swift.snap +++ /dev/null @@ -1,353 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -import CWeaveFFI -import Foundation - -public enum WeaveFFIError: Error, LocalizedError { - case error(code: Int32, message: String) - public var errorDescription: String? { - switch self { - case let .error(_, message): return message - } - } - public var errorCode: Int32 { - switch self { - case let .error(code, _): return code - } - } -} - -@inline(__always) -func check(_ err: inout weaveffi_error) throws { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - weaveffi_error_clear(&err) - throw WeaveFFIError.error(code: code, message: message) - } -} - -@inline(__always) -func trap(_ err: inout weaveffi_error) { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - weaveffi_error_clear(&err) - fatalError("\(code): \(message)") - } -} - -/// Serializes values into the WeaveFFI value-buffer wire format -/// (little-endian, packed, no alignment). -struct WvWriter { - var bytes: [UInt8] = [] - - mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } - mutating func writeInt8(_ v: Int8) { bytes.append(UInt8(bitPattern: v)) } - mutating func writeUInt8(_ v: UInt8) { bytes.append(v) } - mutating func writeUInt16(_ v: UInt16) { - bytes.append(UInt8(truncatingIfNeeded: v)) - bytes.append(UInt8(truncatingIfNeeded: v >> 8)) - } - mutating func writeInt16(_ v: Int16) { writeUInt16(UInt16(bitPattern: v)) } - mutating func writeUInt32(_ v: UInt32) { - bytes.append(UInt8(truncatingIfNeeded: v)) - bytes.append(UInt8(truncatingIfNeeded: v >> 8)) - bytes.append(UInt8(truncatingIfNeeded: v >> 16)) - bytes.append(UInt8(truncatingIfNeeded: v >> 24)) - } - mutating func writeInt32(_ v: Int32) { writeUInt32(UInt32(bitPattern: v)) } - mutating func writeUInt64(_ v: UInt64) { - writeUInt32(UInt32(truncatingIfNeeded: v)) - writeUInt32(UInt32(truncatingIfNeeded: v >> 32)) - } - mutating func writeInt64(_ v: Int64) { writeUInt64(UInt64(bitPattern: v)) } - mutating func writeFloat(_ v: Float) { writeUInt32(v.bitPattern) } - mutating func writeDouble(_ v: Double) { writeUInt64(v.bitPattern) } - mutating func writeLen(_ n: Int) { - precondition(n >= 0 && n <= Int(UInt32.max), "WeaveFFI buffer length exceeds UInt32.max") - writeUInt32(UInt32(n)) - } - mutating func writeString(_ v: String) { - let utf8 = Array(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) } -} - -/// Traps on a malformed value buffer. Per the wire-format spec, consumers -/// surface decode failures through the same channel as a producer panic. -func wvDecodeFailure(_ context: String) -> Never { - fatalError("malformed WeaveFFI value buffer: \(context)") -} - -/// Deserializes values from the WeaveFFI value-buffer wire format, rejecting -/// truncated buffers, invalid flag bytes, oversized length prefixes, and -/// trailing bytes. -struct WvReader { - let bytes: [UInt8] - var pos: Int = 0 - - init(bytes: [UInt8]) { self.bytes = bytes } - - var remaining: Int { bytes.count - pos } - - mutating func take(_ n: Int, _ context: String) -> ArraySlice { - guard remaining >= n else { wvDecodeFailure(context) } - defer { pos += n } - return bytes[pos..<(pos + n)] - } - - mutating func readBool() -> Bool { - switch take(1, "bool").first! { - case 0: return false - case 1: return true - default: wvDecodeFailure("bool byte out of range") - } - } - mutating func readUInt8() -> UInt8 { take(1, "u8").first! } - mutating func readInt8() -> Int8 { Int8(bitPattern: readUInt8()) } - mutating func readUInt16() -> UInt16 { - var v: UInt16 = 0 - for (i, b) in take(2, "u16").enumerated() { v |= UInt16(b) << (8 * i) } - return v - } - mutating func readInt16() -> Int16 { Int16(bitPattern: readUInt16()) } - mutating func readUInt32() -> UInt32 { - var v: UInt32 = 0 - for (i, b) in take(4, "u32").enumerated() { v |= UInt32(b) << (8 * i) } - return v - } - mutating func readInt32() -> Int32 { Int32(bitPattern: readUInt32()) } - mutating func readUInt64() -> UInt64 { - var v: UInt64 = 0 - for (i, b) in take(8, "u64").enumerated() { v |= UInt64(b) << (8 * i) } - return v - } - mutating func readInt64() -> Int64 { Int64(bitPattern: readUInt64()) } - mutating func readFloat() -> Float { Float(bitPattern: readUInt32()) } - mutating func readDouble() -> Double { Double(bitPattern: readUInt64()) } - mutating func readLen() -> Int { - let n = Int(readUInt32()) - guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } - return n - } - mutating func readString() -> String { - let n = readLen() - guard let s = String(bytes: take(n, "string bytes"), encoding: .utf8) else { - wvDecodeFailure("string is not valid UTF-8") - } - return s - } - mutating func readBytes() -> Data { - let n = readLen() - return Data(take(n, "byte buffer")) - } - mutating func readOptionFlag() -> Bool { - switch take(1, "option flag").first! { - case 0: return false - case 1: return true - default: wvDecodeFailure("option flag byte out of range") - } - } - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -/// Typed errors reported by the `contacts` module. -public enum ContactsError: Error, LocalizedError { - case invalidName(message: String) - case notFound(message: String) - public var errorDescription: String? { - switch self { - case let .invalidName(message): return message - case let .notFound(message): return message - } - } - /// The numeric ABI code carried by this error. - public var errorCode: Int32 { - switch self { - case .invalidName: return 1 - case .notFound: return 2 - } - } -} - -@inline(__always) -func mapContacts(code: Int32, message: String, payload: [UInt8]?) -> Error { - switch code { - case 1: return ContactsError.invalidName(message: message.isEmpty ? "name must not be empty" : message) - case 2: return ContactsError.notFound(message: message.isEmpty ? "contact not found" : message) - default: return WeaveFFIError.error(code: code, message: message) - } -} - -@inline(__always) -func checkContacts(_ err: inout weaveffi_error) throws { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - let payload: [UInt8]? = err.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.payload_len)) } - weaveffi_error_clear(&err) - throw mapContacts(code: code, message: message, payload: payload) - } -} - -public enum ContactType: UInt32 { - case personal = 0 - case work = 1 - case other = 2 -} - -public struct Contact { - public var id: Int64 - public var firstName: String - public var lastName: String - public var email: String? - public var contactType: ContactType - - /// Creates a `Contact` value. - public init(id: Int64, firstName: String, lastName: String, email: String?, contactType: ContactType) { - self.id = id - self.firstName = firstName - self.lastName = lastName - self.email = email - self.contactType = contactType - } -} - -/// Serializes a `Contact` into the value-buffer wire format. -func wvWriteContact(_ value: Contact, into w: inout WvWriter) { - w.writeInt64(value.id) - w.writeString(value.firstName) - w.writeString(value.lastName) - if let v0 = value.email { - w.writeOptionFlag(true) - w.writeString(v0) - } else { - w.writeOptionFlag(false) - } - w.writeInt32(Int32(bitPattern: value.contactType.rawValue)) -} - -/// Deserializes a `Contact` from the value-buffer wire format. -func wvReadContact(_ r: inout WvReader) -> Contact { - let v1 = r.readInt64() - let v2 = r.readString() - let v3 = r.readString() - var v4: String? = nil - if r.readOptionFlag() { - let v5 = r.readString() - v4 = v5 - } - let v6 = ContactType(rawValue: UInt32(bitPattern: r.readInt32()))! - return Contact(id: v1, firstName: v2, lastName: v3, email: v4, contactType: v6) -} - -/// An in-memory address book owning its contacts -public final class ContactBook { - let ptr: OpaquePointer - - init(ptr: OpaquePointer) { - self.ptr = ptr - } - - deinit { - weaveffi_contacts_ContactBook_destroy(ptr) - } - public init() { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_contacts_ContactBook_new(&err) - trap(&err) - guard let rv = rv else { fatalError("-1: null pointer") } - self.ptr = rv - } - public func add(firstName: String, lastName: String, email: String?, contactType: ContactType) throws -> Contact { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var emailWriter = WvWriter() - if let v0 = email { - emailWriter.writeOptionFlag(true) - emailWriter.writeString(v0) - } else { - emailWriter.writeOptionFlag(false) - } - var outLen: Int = 0 - let rv: UnsafePointer? = firstName.withCString { firstName_ptr in - return lastName.withCString { lastName_ptr in - return emailWriter.bytes.withUnsafeBufferPointer { email_buf in - let email_ptr = email_buf.baseAddress - let email_len = email_buf.count - return weaveffi_contacts_ContactBook_add(ptr, firstName_ptr, lastName_ptr, email_ptr, email_len, weaveffi_contacts_ContactType(contactType.rawValue), &outLen, &err) - } - } - } - try checkContacts(&err) - guard let rv = rv else { throw WeaveFFIError.error(code: -1, message: "null buffer") } - let rvBytes = [UInt8](UnsafeBufferPointer(start: rv, count: outLen)) - weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen) - var rvReader = WvReader(bytes: rvBytes) - let v1 = wvReadContact(&rvReader) - rvReader.finish() - return v1 - } - public func get(id: Int64) throws -> Contact { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_contacts_ContactBook_get(ptr, id, &outLen, &err) - try checkContacts(&err) - guard let rv = rv else { throw WeaveFFIError.error(code: -1, message: "null buffer") } - let rvBytes = [UInt8](UnsafeBufferPointer(start: rv, count: outLen)) - weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen) - var rvReader = WvReader(bytes: rvBytes) - let v0 = wvReadContact(&rvReader) - rvReader.finish() - return v0 - } - public func list() -> [Contact] { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_contacts_ContactBook_list(ptr, &outLen, &err) - trap(&err) - guard let rv = rv else { fatalError("-1: null buffer") } - let rvBytes = [UInt8](UnsafeBufferPointer(start: rv, count: outLen)) - weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen) - var rvReader = WvReader(bytes: rvBytes) - let n1 = rvReader.readLen() - var v0: [Contact] = [] - v0.reserveCapacity(n1) - for _ in 0.. Bool { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_contacts_ContactBook_remove(ptr, id, &err) - trap(&err) - return rv - } - public func count() -> Int32 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_contacts_ContactBook_count(ptr, &err) - trap(&err) - return rv - } -} - -public enum Contacts { -} - -// END WeaveFFI.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_async_demo__Package_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Package_swift.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/swift_async_demo__Package_swift.snap rename to crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Package_swift.snap diff --git a/crates/weaveffi-cli/tests/snapshots/swift_async_demo__Sources_CWeaveFFI_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_CWeaveFFI_module_modulemap.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/swift_async_demo__Sources_CWeaveFFI_module_modulemap.snap rename to crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_CWeaveFFI_module_modulemap.snap diff --git a/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_WeaveFFI_WeaveFFI_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_WeaveFFI_WeaveFFI_swift.snap new file mode 100644 index 00000000..390ae646 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/swift_edge_cases__Sources_WeaveFFI_WeaveFFI_swift.snap @@ -0,0 +1,1462 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from weaveffi.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate weaveffi.yml -o + +import CWeaveFFI +import Foundation + +public enum WeaveFFIError: Error, LocalizedError { + case error(code: Int32, message: String) + public var errorDescription: String? { + switch self { + case let .error(_, message): return message + } + } + public var errorCode: Int32 { + switch self { + case let .error(code, _): return code + } + } +} + +@inline(__always) +func check(_ err: inout weaveffi_error) throws { + if err.code != 0 { + let code = err.code + let message = err.message.flatMap { String(cString: $0) } ?? "" + weaveffi_error_clear(&err) + throw WeaveFFIError.error(code: code, message: message) + } +} + +@inline(__always) +func trap(_ err: inout weaveffi_error) { + if err.code != 0 { + let code = err.code + let message = err.message.flatMap { String(cString: $0) } ?? "" + weaveffi_error_clear(&err) + fatalError("\(code): \(message)") + } +} + +/// Serializes values into the WeaveFFI value-buffer wire format +/// (little-endian, packed, no alignment). +struct WvWriter { + var bytes: [UInt8] = [] + + mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } + mutating func writeInt8(_ v: Int8) { bytes.append(UInt8(bitPattern: v)) } + mutating func writeUInt8(_ v: UInt8) { bytes.append(v) } + mutating func writeUInt16(_ v: UInt16) { + bytes.append(UInt8(truncatingIfNeeded: v)) + bytes.append(UInt8(truncatingIfNeeded: v >> 8)) + } + mutating func writeInt16(_ v: Int16) { writeUInt16(UInt16(bitPattern: v)) } + mutating func writeUInt32(_ v: UInt32) { + bytes.append(UInt8(truncatingIfNeeded: v)) + bytes.append(UInt8(truncatingIfNeeded: v >> 8)) + bytes.append(UInt8(truncatingIfNeeded: v >> 16)) + bytes.append(UInt8(truncatingIfNeeded: v >> 24)) + } + mutating func writeInt32(_ v: Int32) { writeUInt32(UInt32(bitPattern: v)) } + mutating func writeUInt64(_ v: UInt64) { + writeUInt32(UInt32(truncatingIfNeeded: v)) + writeUInt32(UInt32(truncatingIfNeeded: v >> 32)) + } + mutating func writeInt64(_ v: Int64) { writeUInt64(UInt64(bitPattern: v)) } + mutating func writeFloat(_ v: Float) { writeUInt32(v.bitPattern) } + mutating func writeDouble(_ v: Double) { writeUInt64(v.bitPattern) } + mutating func writeLen(_ n: Int) { + precondition(n >= 0 && n <= Int(UInt32.max), "WeaveFFI buffer length exceeds UInt32.max") + writeUInt32(UInt32(n)) + } + mutating func writeString(_ v: String) { + let utf8 = Array(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) } +} + +/// Traps on a malformed value buffer. Per the wire-format spec, consumers +/// surface decode failures through the same channel as a producer panic. +func wvDecodeFailure(_ context: String) -> Never { + fatalError("malformed WeaveFFI value buffer: \(context)") +} + +/// Deserializes values from the WeaveFFI value-buffer wire format, rejecting +/// truncated buffers, invalid flag bytes, oversized length prefixes, and +/// trailing bytes. +struct WvReader { + let bytes: [UInt8] + var pos: Int = 0 + + init(bytes: [UInt8]) { self.bytes = bytes } + + var remaining: Int { bytes.count - pos } + + mutating func take(_ n: Int, _ context: String) -> ArraySlice { + guard remaining >= n else { wvDecodeFailure(context) } + defer { pos += n } + return bytes[pos..<(pos + n)] + } + + mutating func readBool() -> Bool { + switch take(1, "bool").first! { + case 0: return false + case 1: return true + default: wvDecodeFailure("bool byte out of range") + } + } + mutating func readUInt8() -> UInt8 { take(1, "u8").first! } + mutating func readInt8() -> Int8 { Int8(bitPattern: readUInt8()) } + mutating func readUInt16() -> UInt16 { + var v: UInt16 = 0 + for (i, b) in take(2, "u16").enumerated() { v |= UInt16(b) << (8 * i) } + return v + } + mutating func readInt16() -> Int16 { Int16(bitPattern: readUInt16()) } + mutating func readUInt32() -> UInt32 { + var v: UInt32 = 0 + for (i, b) in take(4, "u32").enumerated() { v |= UInt32(b) << (8 * i) } + return v + } + mutating func readInt32() -> Int32 { Int32(bitPattern: readUInt32()) } + mutating func readUInt64() -> UInt64 { + var v: UInt64 = 0 + for (i, b) in take(8, "u64").enumerated() { v |= UInt64(b) << (8 * i) } + return v + } + mutating func readInt64() -> Int64 { Int64(bitPattern: readUInt64()) } + mutating func readFloat() -> Float { Float(bitPattern: readUInt32()) } + mutating func readDouble() -> Double { Double(bitPattern: readUInt64()) } + mutating func readLen() -> Int { + let n = Int(readUInt32()) + guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } + return n + } + mutating func readString() -> String { + let n = readLen() + guard let s = String(bytes: take(n, "string bytes"), encoding: .utf8) else { + wvDecodeFailure("string is not valid UTF-8") + } + return s + } + mutating func readBytes() -> Data { + let n = readLen() + return Data(take(n, "byte buffer")) + } + mutating func readOptionFlag() -> Bool { + switch take(1, "option flag").first! { + case 0: return false + case 1: return true + default: wvDecodeFailure("option flag byte out of range") + } + } + func finish() { + if remaining != 0 { wvDecodeFailure("trailing bytes after value") } + } +} + +private final class ContinuationRef { + let value: CheckedContinuation + init(_ value: CheckedContinuation) { self.value = value } +} + +final class WvCallbackBox { + let value: T + init(_ value: T) { self.value = value } +} + +var wvListenerContexts: [UInt64: UnsafeMutableRawPointer] = [:] +let wvListenerLock = NSLock() + +/// Typed errors reported by the `edge_cases` module. +public enum EdgeErrorsError: Error, LocalizedError { + case busy(message: String) + 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 + } + } +} + +@inline(__always) +func mapEdgeCases(code: Int32, message: String, payload: [UInt8]?) -> Error { + switch code { + case 7: return EdgeErrorsError.busy(message: message.isEmpty ? "Resource is busy" : message) + default: return WeaveFFIError.error(code: code, message: message) + } +} + +@inline(__always) +func checkEdgeCases(_ err: inout weaveffi_error) throws { + if err.code != 0 { + let code = err.code + let message = err.message.flatMap { String(cString: $0) } ?? "" + let payload: [UInt8]? = err.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.payload_len)) } + weaveffi_error_clear(&err) + throw mapEdgeCases(code: code, message: message, payload: payload) + } +} + +/// Deprecated enum with non-contiguous values and keyword-like variants +public enum Legacy: UInt32 { + case none = 0 + case default_ = 5 + case class_ = 10 +} + +public enum Mode: UInt32 { + case fast = 1 + case safe = 2 +} + +public struct Leaf { + public var weight: Float + + /// Creates a `Leaf` value. + public init(weight: Float) { + self.weight = weight + } +} + +/// Serializes a `Leaf` into the value-buffer wire format. +func wvWriteLeaf(_ value: Leaf, into w: inout WvWriter) { + w.writeFloat(value.weight) +} + +/// Deserializes a `Leaf` from the value-buffer wire format. +func wvReadLeaf(_ r: inout WvReader) -> Leaf { + let v0 = r.readFloat() + return Leaf(weight: v0) +} + +/// Middle layer of a three-deep struct nesting +public struct Inner { + public var leaf: Leaf + public var leaves: [Leaf] + + /// Creates a `Inner` value. + public init(leaf: Leaf, leaves: [Leaf]) { + self.leaf = leaf + self.leaves = leaves + } +} + +/// Serializes a `Inner` into the value-buffer wire format. +func wvWriteInner(_ value: Inner, into w: inout WvWriter) { + wvWriteLeaf(value.leaf, into: &w) + w.writeLen(value.leaves.count) + for v0 in value.leaves { + wvWriteLeaf(v0, into: &w) + } +} + +/// Deserializes a `Inner` from the value-buffer wire format. +func wvReadInner(_ r: inout WvReader) -> Inner { + let v1 = wvReadLeaf(&r) + let n3 = r.readLen() + var v2: [Leaf] = [] + v2.reserveCapacity(n3) + for _ in 0.. Outer { + let v12 = wvReadInner(&r) + var v13: Inner? = nil + if r.readOptionFlag() { + let v14 = wvReadInner(&r) + v13 = v14 + } + let n16 = r.readLen() + var v15: [String: Inner] = [:] + v15.reserveCapacity(n16) + for _ in 0.. Keywords { + let v0 = r.readInt32() + let v1 = r.readInt32() + let v2 = r.readInt32() + let v3 = r.readInt32() + let v4 = r.readInt32() + let v5 = r.readInt32() + let v6 = r.readInt32() + let v7 = r.readInt32() + let v8 = r.readInt32() + let v9 = r.readInt32() + let v10 = r.readInt32() + let v11 = r.readInt32() + let v12 = r.readInt32() + let v13 = r.readInt32() + let v14 = r.readInt32() + let v15 = r.readInt32() + let v16 = r.readInt32() + let v17 = r.readInt32() + let v18 = r.readInt32() + let v19 = r.readInt32() + let v20 = r.readInt32() + let v21 = r.readInt32() + let v22 = r.readInt32() + let v23 = r.readInt32() + let v24 = r.readInt32() + let v25 = r.readInt32() + let v26 = r.readInt32() + let v27 = r.readInt32() + let v28 = r.readInt32() + let v29 = r.readInt32() + let v30 = r.readInt32() + let v31 = r.readInt32() + let v32 = r.readInt32() + let v33 = r.readInt32() + let v34 = r.readInt32() + let v35 = r.readInt32() + let v36 = r.readInt32() + let v37 = r.readInt32() + let v38 = r.readInt32() + let v39 = r.readInt32() + let v40 = r.readInt32() + let v41 = r.readInt32() + let v42 = r.readInt32() + let v43 = r.readInt32() + let v44 = r.readInt32() + let v45 = r.readInt32() + let v46 = r.readInt32() + let v47 = r.readInt32() + let v48 = r.readInt32() + let v49 = r.readInt32() + let v50 = r.readInt32() + let v51 = r.readInt32() + let v52 = r.readInt32() + let v53 = r.readInt32() + let v54 = r.readInt32() + let v55 = r.readInt32() + let v56 = r.readInt32() + let v57 = r.readInt32() + let v58 = r.readInt32() + return Keywords(class_: v0, def: v1, import_: v2, from: v3, func_: v4, range: v5, register: v6, volatile: v7, new: v8, default_: v9, end: v10, in_: v11, is_: v12, int: v13, var_: v14, val: v15, self_: v16, this: v17, object: v18, string: v19, case_: v20, select: v21, switch_: v22, void: v23, union: v24, namespace: v25, operator_: v26, template: v27, package: v28, protocol_: v29, guard_: v30, defer_: v31, inout_: v32, internal_: v33, when: v34, fun: v35, data: v36, late: v37, part: v38, out: v39, ref: v40, event: v41, base: v42, lambda: v43, yield: v44, nil_: v45, null: v46, begin: v47, rescue: v48, module: v49, chan: v50, go: v51, let_: v52, export: v53, delete: v54, typedef: v55, sizeof: v56, static_: v57, super_: v58) +} + +/// Deprecated struct +public struct Retired { + public var id: Int64 + + /// Creates a `Retired` value. + public init(id: Int64) { + self.id = id + } +} + +/// Serializes a `Retired` into the value-buffer wire format. +func wvWriteRetired(_ value: Retired, into w: inout WvWriter) { + w.writeInt64(value.id) +} + +/// Deserializes a `Retired` from the value-buffer wire format. +func wvReadRetired(_ r: inout WvReader) -> Retired { + let v0 = r.readInt64() + return Retired(id: v0) +} + +/// Interface with no constructor; instances come from make_widget +public final class Widget { + let ptr: OpaquePointer + + init(ptr: OpaquePointer) { + self.ptr = ptr + } + + deinit { + weaveffi_edge_cases_Widget_destroy(ptr) + } + /// Void method that can fail + public func refresh() throws -> Void { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + weaveffi_edge_cases_Widget_refresh(ptr, &err) + try checkEdgeCases(&err) + } + public func rename(class_: String, import_: String?) -> String { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + var import_Writer = WvWriter() + if let v0 = import_ { + import_Writer.writeOptionFlag(true) + import_Writer.writeString(v0) + } else { + import_Writer.writeOptionFlag(false) + } + let rv: UnsafePointer? = class_.withCString { class__ptr in + return import_Writer.bytes.withUnsafeBufferPointer { import__buf in + let import__ptr = import__buf.baseAddress + let import__len = import__buf.count + return weaveffi_edge_cases_Widget_rename(ptr, class__ptr, import__ptr, import__len, &err) + } + } + trap(&err) + guard let rv = rv else { fatalError("-1: null string") } + defer { weaveffi_free_string(rv) } + return String(cString: rv) + } + /// Async method with a struct result + public func fetch(id: Int64) async throws -> Outer { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque() + weaveffi_edge_cases_Widget_fetch_async(ptr, id, { context, err, resultPtr, resultLen in + let contRef = Unmanaged>.fromOpaque(context!).takeRetainedValue() + if let err = err, err.pointee.code != 0 { + let code = err.pointee.code + let msg = err.pointee.message.flatMap { String(cString: $0) } ?? "" + let payload: [UInt8]? = err.pointee.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.pointee.payload_len)) } + weaveffi_error_free(err) + contRef.value.resume(throwing: mapEdgeCases(code: code, message: msg, payload: payload)) + } else { + guard let resultPtr = resultPtr else { + contRef.value.resume(throwing: WeaveFFIError.error(code: -1, message: "null buffer")) + return + } + let resultBytes = [UInt8](UnsafeBufferPointer(start: resultPtr, count: resultLen)) + weaveffi_free_bytes(UnsafeMutablePointer(mutating: resultPtr), resultLen) + var resultReader = WvReader(bytes: resultBytes) + let v0 = wvReadOuter(&resultReader) + resultReader.finish() + contRef.value.resume(returning: v0) + } + }, ctx) + } + } + @available(*, deprecated, message: "Use rename instead") + public func oldName() -> String { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + let rv = weaveffi_edge_cases_Widget_old_name(ptr, &err) + trap(&err) + guard let rv = rv else { fatalError("-1: null string") } + defer { weaveffi_free_string(rv) } + return String(cString: rv) + } +} + +/// Deprecated interface with a throwing constructor +public final class Vault { + let ptr: OpaquePointer + + init(ptr: OpaquePointer) { + self.ptr = ptr + } + + deinit { + weaveffi_edge_cases_Vault_destroy(ptr) + } + /// Fails when the vault is busy + public static func open(path: String) throws -> Vault { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + let rv: OpaquePointer? = path.withCString { path_ptr in + return weaveffi_edge_cases_Vault_open(path_ptr, &err) + } + try checkEdgeCases(&err) + guard let rv = rv else { throw WeaveFFIError.error(code: -1, message: "null pointer") } + return Vault(ptr: rv) + } + public func peek() -> Widget? { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + let rv = weaveffi_edge_cases_Vault_peek(ptr, &err) + trap(&err) + return rv.map { Widget(ptr: $0) } + } + /// Throwing static + public static func probe() throws -> Bool { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + let rv = weaveffi_edge_cases_Vault_probe(&err) + try checkEdgeCases(&err) + return rv + } +} + +/// A lazy sequence over the `String` elements streamed by `weaveffi_edge_cases_string_stream`. +/// +/// Each `next()` call pulls exactly one element from the producer. The +/// underlying C 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 } + weaveffi_edge_cases_StringStreamIterator_destroy(handle) + self.handle = nil + } + + /// Pulls the next element from the producer, 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 err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + if weaveffi_edge_cases_StringStreamIterator_next(handle, &item, &err) == 0 { + if err.code != 0 { + let code = err.code + let message = err.message.flatMap { String(cString: $0) } ?? "" + weaveffi_error_clear(&err) + fatalError("\(code): \(message)") + } + destroyHandle() + return nil + } + let element = String(cString: item!) + weaveffi_free_string(item) + return element + } +} + +/// A lazy sequence over the `Int32` elements streamed by `weaveffi_edge_cases_scalar_stream`. +/// +/// Each `next()` call pulls exactly one element from the producer. The +/// underlying C 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 } + weaveffi_edge_cases_ScalarStreamIterator_destroy(handle) + self.handle = nil + } + + /// Pulls the next element from the producer, 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 = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + if weaveffi_edge_cases_ScalarStreamIterator_next(handle, &item, &err) == 0 { + if err.code != 0 { + let code = err.code + let message = err.message.flatMap { String(cString: $0) } ?? "" + weaveffi_error_clear(&err) + fatalError("\(code): \(message)") + } + destroyHandle() + return nil + } + let element = item + return element + } +} + +/// A lazy sequence over the `Mode` elements streamed by `weaveffi_edge_cases_mode_stream`. +/// +/// Each `next()` call pulls exactly one element from the producer. The +/// underlying C 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 } + weaveffi_edge_cases_ModeStreamIterator_destroy(handle) + self.handle = nil + } + + /// Pulls the next element from the producer, 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: weaveffi_edge_cases_Mode = weaveffi_edge_cases_Mode(0) + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + if weaveffi_edge_cases_ModeStreamIterator_next(handle, &item, &err) == 0 { + if err.code != 0 { + let code = err.code + let message = err.message.flatMap { String(cString: $0) } ?? "" + weaveffi_error_clear(&err) + fatalError("\(code): \(message)") + } + destroyHandle() + return nil + } + let element = Mode(rawValue: item.rawValue)! + return element + } +} + +public enum EdgeCases { + /// - Returns: A subscription id for ``unregisterBatchListener(_:)``. + public static func registerBatchListener(_ callback: @escaping ([Outer], Data, Outer?, Mode) -> Void) -> UInt64 { + let box = WvCallbackBox(callback) + let ctx = Unmanaged.passRetained(box).toOpaque() + let id = weaveffi_edge_cases_register_batch_listener({ items_ptr, items_len, raw_ptr, raw_len, first_ptr, first_len, mode, context in + let cb = Unmanaged Void>>.fromOpaque(context!).takeUnretainedValue().value + let itemsBuf = [UInt8](UnsafeBufferPointer(start: items_ptr, count: items_len)) + var itemsReader = WvReader(bytes: itemsBuf) + let n1 = itemsReader.readLen() + var v0: [Outer] = [] + v0.reserveCapacity(n1) + for _ in 0.. Void>>.fromOpaque(ctx).release() + } + } + /// - Returns: A subscription id for ``unregisterTickListener(_:)``. + public static func registerTickListener(_ callback: @escaping () -> Void) -> UInt64 { + let box = WvCallbackBox(callback) + let ctx = Unmanaged.passRetained(box).toOpaque() + let id = weaveffi_edge_cases_register_tick_listener({ context in + let cb = Unmanaged Void>>.fromOpaque(context!).takeUnretainedValue().value + cb() + }, ctx) + wvListenerLock.lock() + wvListenerContexts[id] = ctx + wvListenerLock.unlock() + return id + } + /// Unregisters a listener previously registered with ``registerTickListener(_:)``. + public static func unregisterTickListener(_ id: UInt64) { + weaveffi_edge_cases_unregister_tick_listener(id) + wvListenerLock.lock() + let ctx = wvListenerContexts.removeValue(forKey: id) + wvListenerLock.unlock() + if let ctx = ctx { + Unmanaged Void>>.fromOpaque(ctx).release() + } + } + /// No parameters and no result + public static func noop() -> Void { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + weaveffi_edge_cases_noop(&err) + trap(&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 { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + var func_Writer = WvWriter() + func_Writer.writeLen(func_.count) + for v0 in func_ { + func_Writer.writeString(v0) + } + var rangeWriter = WvWriter() + if let v1 = range { + rangeWriter.writeOptionFlag(true) + rangeWriter.writeInt32(v1) + } else { + rangeWriter.writeOptionFlag(false) + } + let rv: Int32 = from.withCString { from_ptr in + return func_Writer.bytes.withUnsafeBufferPointer { func__buf in + let func__ptr = func__buf.baseAddress + let func__len = func__buf.count + return rangeWriter.bytes.withUnsafeBufferPointer { range_buf in + let range_ptr = range_buf.baseAddress + let range_len = range_buf.count + return weaveffi_edge_cases_select(class_, from_ptr, func__ptr, func__len, range_ptr, range_len, type, &err) + } + } + } + trap(&err) + return rv + } + /// Optional list of optional structs, the deepest legal nesting + public static func nestedOptional(items: [Outer?]?) -> [Outer?]? { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + var itemsWriter = WvWriter() + if let v0 = items { + itemsWriter.writeOptionFlag(true) + itemsWriter.writeLen(v0.count) + for v1 in v0 { + if let v2 = v1 { + itemsWriter.writeOptionFlag(true) + wvWriteOuter(v2, into: &itemsWriter) + } else { + itemsWriter.writeOptionFlag(false) + } + } + } else { + itemsWriter.writeOptionFlag(false) + } + var outLen: Int = 0 + let rv: UnsafePointer? = itemsWriter.bytes.withUnsafeBufferPointer { items_buf in + let items_ptr = items_buf.baseAddress + let items_len = items_buf.count + return weaveffi_edge_cases_nested_optional(items_ptr, items_len, &outLen, &err) + } + trap(&err) + guard let rv = rv else { fatalError("-1: null buffer") } + let rvBytes = [UInt8](UnsafeBufferPointer(start: rv, count: outLen)) + weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen) + var rvReader = WvReader(bytes: rvBytes) + var v3: [Outer?]? = nil + if rvReader.readOptionFlag() { + let n5 = rvReader.readLen() + var v4: [Outer?] = [] + v4.reserveCapacity(n5) + for _ in 0.. Mode? { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + var textWriter = WvWriter() + if let v0 = text { + textWriter.writeOptionFlag(true) + textWriter.writeString(v0) + } else { + textWriter.writeOptionFlag(false) + } + var countWriter = WvWriter() + if let v1 = count { + countWriter.writeOptionFlag(true) + countWriter.writeInt32(v1) + } else { + countWriter.writeOptionFlag(false) + } + var outerWriter = WvWriter() + if let v2 = outer { + outerWriter.writeOptionFlag(true) + wvWriteOuter(v2, into: &outerWriter) + } else { + outerWriter.writeOptionFlag(false) + } + var tagsWriter = WvWriter() + if let v3 = tags { + tagsWriter.writeOptionFlag(true) + tagsWriter.writeLen(v3.count) + for v4 in v3 { + tagsWriter.writeString(v4) + } + } else { + tagsWriter.writeOptionFlag(false) + } + var blobWriter = WvWriter() + if let v5 = blob { + blobWriter.writeOptionFlag(true) + blobWriter.writeBytes(v5) + } else { + blobWriter.writeOptionFlag(false) + } + var modeWriter = WvWriter() + if let v6 = mode { + modeWriter.writeOptionFlag(true) + modeWriter.writeInt32(Int32(bitPattern: v6.rawValue)) + } else { + modeWriter.writeOptionFlag(false) + } + var outLen: Int = 0 + let rv: UnsafePointer? = textWriter.bytes.withUnsafeBufferPointer { text_buf in + let text_ptr = text_buf.baseAddress + let text_len = text_buf.count + return countWriter.bytes.withUnsafeBufferPointer { count_buf in + let count_ptr = count_buf.baseAddress + let count_len = count_buf.count + return outerWriter.bytes.withUnsafeBufferPointer { outer_buf in + let outer_ptr = outer_buf.baseAddress + let outer_len = outer_buf.count + return tagsWriter.bytes.withUnsafeBufferPointer { tags_buf in + let tags_ptr = tags_buf.baseAddress + let tags_len = tags_buf.count + return blobWriter.bytes.withUnsafeBufferPointer { blob_buf in + let blob_ptr = blob_buf.baseAddress + let blob_len = blob_buf.count + return modeWriter.bytes.withUnsafeBufferPointer { mode_buf in + let mode_ptr = mode_buf.baseAddress + let mode_len = mode_buf.count + return weaveffi_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) + } + } + } + } + } + } + trap(&err) + guard let rv = rv else { fatalError("-1: null buffer") } + let rvBytes = [UInt8](UnsafeBufferPointer(start: rv, count: outLen)) + weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen) + var rvReader = WvReader(bytes: rvBytes) + var v7: Mode? = nil + if rvReader.readOptionFlag() { + let v8 = Mode(rawValue: UInt32(bitPattern: rvReader.readInt32()))! + v7 = v8 + } + rvReader.finish() + return v7 + } + /// Write-back parameters + public static func mutableParams(text: String, blob: Data) -> Void { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + let blobBytes = Array(blob) + text.withCString { text_ptr in + blobBytes.withUnsafeBufferPointer { blob_buf in + let blob_ptr = blob_buf.baseAddress + let blob_len = blob_buf.count + weaveffi_edge_cases_mutable_params(text_ptr, blob_ptr, blob_len, &err) + } + } + trap(&err) + } + /// Factory for an interface with no constructor + public static func makeWidget(label: String) -> Widget { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + let rv: OpaquePointer? = label.withCString { label_ptr in + return weaveffi_edge_cases_make_widget(label_ptr, &err) + } + trap(&err) + guard let rv = rv else { fatalError("-1: null pointer") } + return Widget(ptr: rv) + } + /// Optional interface return + public static func findWidget(id: Int64) throws -> Widget? { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + let rv = weaveffi_edge_cases_find_widget(id, &err) + try checkEdgeCases(&err) + return rv.map { Widget(ptr: $0) } + } + /// Interface parameter, including an optional one + public static func inspectWidget(widget: Widget, fallback: Widget?) -> String { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + let rv = weaveffi_edge_cases_inspect_widget(widget.ptr, fallback?.ptr, &err) + trap(&err) + guard let rv = rv else { fatalError("-1: null string") } + defer { weaveffi_free_string(rv) } + return String(cString: rv) + } + public static func typedHandleParam(token: UInt64, raw: UInt64) -> UInt64 { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + let rv = weaveffi_edge_cases_typed_handle_param(OpaquePointer(bitPattern: UInt(token)), raw, &err) + trap(&err) + return UInt64(UInt(bitPattern: rv)) + } + /// Iterator of strings + /// - Returns: A lazy sequence that pulls one element per step from the + /// producer; the underlying iterator is destroyed when the sequence is + /// exhausted or deinitialized. + public static func stringStream() -> EdgeCasesStringStreamIterator { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + let rv = weaveffi_edge_cases_string_stream(&err) + trap(&err) + guard let rv = rv else { fatalError("-1: null iterator") } + return EdgeCasesStringStreamIterator(handle: rv) + } + /// Iterator of scalars + /// - Returns: A lazy sequence that pulls one element per step from the + /// producer; the underlying iterator is destroyed when the sequence is + /// exhausted or deinitialized. + public static func scalarStream() -> EdgeCasesScalarStreamIterator { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + let rv = weaveffi_edge_cases_scalar_stream(&err) + trap(&err) + guard let rv = rv else { fatalError("-1: null iterator") } + return EdgeCasesScalarStreamIterator(handle: rv) + } + /// Iterator of enums + /// - Returns: A lazy sequence that pulls one element per step from the + /// producer; the underlying iterator is destroyed when the sequence is + /// exhausted or deinitialized. + public static func modeStream() -> EdgeCasesModeStreamIterator { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + let rv = weaveffi_edge_cases_mode_stream(&err) + trap(&err) + guard let rv = rv else { fatalError("-1: null iterator") } + return EdgeCasesModeStreamIterator(handle: rv) + } + /// Async with no result + public static func asyncVoid() async -> Void { + await withCheckedContinuation { (continuation: CheckedContinuation) in + let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque() + weaveffi_edge_cases_async_void_async({ context, err in + let contRef = Unmanaged>.fromOpaque(context!).takeRetainedValue() + if let err = err, err.pointee.code != 0 { + let code = err.pointee.code + let msg = err.pointee.message.flatMap { String(cString: $0) } ?? "" + weaveffi_error_free(err) + fatalError("\(code): \(msg)") + } else { + contRef.value.resume(returning: ()) + } + }, ctx) + } + } + public static func asyncBytes(size: UInt32) async -> Data { + await withCheckedContinuation { (continuation: CheckedContinuation) in + let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque() + weaveffi_edge_cases_async_bytes_async(size, { context, err, result, resultLen in + let contRef = Unmanaged>.fromOpaque(context!).takeRetainedValue() + if let err = err, err.pointee.code != 0 { + let code = err.pointee.code + let msg = err.pointee.message.flatMap { String(cString: $0) } ?? "" + weaveffi_error_free(err) + fatalError("\(code): \(msg)") + } else { + if let result = result { + let resultValue = Data(bytes: result, count: resultLen) + weaveffi_free_bytes(UnsafeMutablePointer(mutating: result), resultLen) + contRef.value.resume(returning: resultValue) + } else { + contRef.value.resume(returning: Data()) + } + } + }, ctx) + } + } + public static func asyncStruct() async throws -> Outer { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque() + weaveffi_edge_cases_async_struct_async({ context, err, resultPtr, resultLen in + let contRef = Unmanaged>.fromOpaque(context!).takeRetainedValue() + if let err = err, err.pointee.code != 0 { + let code = err.pointee.code + let msg = err.pointee.message.flatMap { String(cString: $0) } ?? "" + let payload: [UInt8]? = err.pointee.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.pointee.payload_len)) } + weaveffi_error_free(err) + contRef.value.resume(throwing: mapEdgeCases(code: code, message: msg, payload: payload)) + } else { + guard let resultPtr = resultPtr else { + contRef.value.resume(throwing: WeaveFFIError.error(code: -1, message: "null buffer")) + return + } + let resultBytes = [UInt8](UnsafeBufferPointer(start: resultPtr, count: resultLen)) + weaveffi_free_bytes(UnsafeMutablePointer(mutating: resultPtr), resultLen) + var resultReader = WvReader(bytes: resultBytes) + let v0 = wvReadOuter(&resultReader) + resultReader.finish() + contRef.value.resume(returning: v0) + } + }, ctx) + } + } + public static func asyncList() async throws -> [Outer] { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[Outer], Error>) in + let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque() + weaveffi_edge_cases_async_list_async(nil, { context, err, resultPtr, resultLen in + let contRef = Unmanaged>.fromOpaque(context!).takeRetainedValue() + if let err = err, err.pointee.code != 0 { + let code = err.pointee.code + let msg = err.pointee.message.flatMap { String(cString: $0) } ?? "" + let payload: [UInt8]? = err.pointee.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.pointee.payload_len)) } + weaveffi_error_free(err) + contRef.value.resume(throwing: mapEdgeCases(code: code, message: msg, payload: payload)) + } else { + guard let resultPtr = resultPtr else { + contRef.value.resume(throwing: WeaveFFIError.error(code: -1, message: "null buffer")) + return + } + let resultBytes = [UInt8](UnsafeBufferPointer(start: resultPtr, count: resultLen)) + weaveffi_free_bytes(UnsafeMutablePointer(mutating: resultPtr), resultLen) + var resultReader = WvReader(bytes: resultBytes) + let n1 = resultReader.readLen() + var v0: [Outer] = [] + v0.reserveCapacity(n1) + for _ in 0.. Int64? { + await withCheckedContinuation { (continuation: CheckedContinuation) in + let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque() + weaveffi_edge_cases_async_optional_async({ context, err, resultPtr, resultLen in + let contRef = Unmanaged>.fromOpaque(context!).takeRetainedValue() + if let err = err, err.pointee.code != 0 { + let code = err.pointee.code + let msg = err.pointee.message.flatMap { String(cString: $0) } ?? "" + weaveffi_error_free(err) + fatalError("\(code): \(msg)") + } else { + guard let resultPtr = resultPtr else { + fatalError("-1: null buffer") + } + let resultBytes = [UInt8](UnsafeBufferPointer(start: resultPtr, count: resultLen)) + weaveffi_free_bytes(UnsafeMutablePointer(mutating: resultPtr), resultLen) + var resultReader = WvReader(bytes: resultBytes) + var v0: Int64? = nil + if resultReader.readOptionFlag() { + let v1 = resultReader.readInt64() + v0 = v1 + } + resultReader.finish() + contRef.value.resume(returning: v0) + } + }, ctx) + } + } + public static func asyncEnum() async -> Mode { + await withCheckedContinuation { (continuation: CheckedContinuation) in + let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque() + weaveffi_edge_cases_async_enum_async({ context, err, result in + let contRef = Unmanaged>.fromOpaque(context!).takeRetainedValue() + if let err = err, err.pointee.code != 0 { + let code = err.pointee.code + let msg = err.pointee.message.flatMap { String(cString: $0) } ?? "" + weaveffi_error_free(err) + fatalError("\(code): \(msg)") + } else { + contRef.value.resume(returning: Mode(rawValue: result.rawValue)!) + } + }, ctx) + } + } + public static func asyncKeywords(class_: String, import_: Int32) async -> String { + await withCheckedContinuation { (continuation: CheckedContinuation) in + let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque() + class_.withCString { class__ptr in + weaveffi_edge_cases_async_keywords_async(class__ptr, import_, { context, err, result in + let contRef = Unmanaged>.fromOpaque(context!).takeRetainedValue() + if let err = err, err.pointee.code != 0 { + let code = err.pointee.code + let msg = err.pointee.message.flatMap { String(cString: $0) } ?? "" + weaveffi_error_free(err) + fatalError("\(code): \(msg)") + } else { + guard let result = result else { + fatalError("-1: null string") + } + let resultValue = String(cString: result) + weaveffi_free_string(result) + contRef.value.resume(returning: resultValue) + } + }, ctx) + } + } + } + public static func keywordsRoundtrip(value: Keywords) -> Keywords { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + var valueWriter = WvWriter() + wvWriteKeywords(value, into: &valueWriter) + var outLen: Int = 0 + let rv: UnsafePointer? = valueWriter.bytes.withUnsafeBufferPointer { value_buf in + let value_ptr = value_buf.baseAddress + let value_len = value_buf.count + return weaveffi_edge_cases_keywords_roundtrip(value_ptr, value_len, &outLen, &err) + } + trap(&err) + guard let rv = rv else { fatalError("-1: null buffer") } + let rvBytes = [UInt8](UnsafeBufferPointer(start: rv, count: outLen)) + weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen) + var rvReader = WvReader(bytes: rvBytes) + let v0 = wvReadKeywords(&rvReader) + rvReader.finish() + return v0 + } + @available(*, deprecated, message: "Use Mode-based APIs") + public static func legacyValue(value: Legacy) -> Retired { + var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) + var outLen: Int = 0 + let rv = weaveffi_edge_cases_legacy_value(weaveffi_edge_cases_Legacy(value.rawValue), &outLen, &err) + trap(&err) + guard let rv = rv else { fatalError("-1: null buffer") } + let rvBytes = [UInt8](UnsafeBufferPointer(start: rv, count: outLen)) + weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen) + var rvReader = WvReader(bytes: rvBytes) + let v0 = wvReadRetired(&rvReader) + rvReader.finish() + return v0 + } +} + +// END WeaveFFI.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_events__Package_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_events__Package_swift.snap deleted file mode 100644 index 7ec6e100..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_events__Package_swift.snap +++ /dev/null @@ -1,23 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// swift-tools-version:5.7 -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -import PackageDescription - -let package = Package( - name: "WeaveFFI", - platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], - products: [ - .library(name: "WeaveFFI", targets: ["WeaveFFI"]), - ], - targets: [ - .systemLibrary(name: "CWeaveFFI"), - .target(name: "WeaveFFI", dependencies: ["CWeaveFFI"]), - ] -) - -// END Package.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_events__Sources_CWeaveFFI_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_events__Sources_CWeaveFFI_module_modulemap.snap deleted file mode 100644 index e97f96e9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_events__Sources_CWeaveFFI_module_modulemap.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -module CWeaveFFI [system] { - header "../../../c/weaveffi.h" - link "weaveffi" - export * -} - -// END module.modulemap diff --git a/crates/weaveffi-cli/tests/snapshots/swift_events__Sources_WeaveFFI_WeaveFFI_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_events__Sources_WeaveFFI_WeaveFFI_swift.snap deleted file mode 100644 index 5bf15621..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_events__Sources_WeaveFFI_WeaveFFI_swift.snap +++ /dev/null @@ -1,265 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -import CWeaveFFI -import Foundation - -public enum WeaveFFIError: Error, LocalizedError { - case error(code: Int32, message: String) - public var errorDescription: String? { - switch self { - case let .error(_, message): return message - } - } - public var errorCode: Int32 { - switch self { - case let .error(code, _): return code - } - } -} - -@inline(__always) -func check(_ err: inout weaveffi_error) throws { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - weaveffi_error_clear(&err) - throw WeaveFFIError.error(code: code, message: message) - } -} - -@inline(__always) -func trap(_ err: inout weaveffi_error) { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - weaveffi_error_clear(&err) - fatalError("\(code): \(message)") - } -} - -/// Serializes values into the WeaveFFI value-buffer wire format -/// (little-endian, packed, no alignment). -struct WvWriter { - var bytes: [UInt8] = [] - - mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } - mutating func writeInt8(_ v: Int8) { bytes.append(UInt8(bitPattern: v)) } - mutating func writeUInt8(_ v: UInt8) { bytes.append(v) } - mutating func writeUInt16(_ v: UInt16) { - bytes.append(UInt8(truncatingIfNeeded: v)) - bytes.append(UInt8(truncatingIfNeeded: v >> 8)) - } - mutating func writeInt16(_ v: Int16) { writeUInt16(UInt16(bitPattern: v)) } - mutating func writeUInt32(_ v: UInt32) { - bytes.append(UInt8(truncatingIfNeeded: v)) - bytes.append(UInt8(truncatingIfNeeded: v >> 8)) - bytes.append(UInt8(truncatingIfNeeded: v >> 16)) - bytes.append(UInt8(truncatingIfNeeded: v >> 24)) - } - mutating func writeInt32(_ v: Int32) { writeUInt32(UInt32(bitPattern: v)) } - mutating func writeUInt64(_ v: UInt64) { - writeUInt32(UInt32(truncatingIfNeeded: v)) - writeUInt32(UInt32(truncatingIfNeeded: v >> 32)) - } - mutating func writeInt64(_ v: Int64) { writeUInt64(UInt64(bitPattern: v)) } - mutating func writeFloat(_ v: Float) { writeUInt32(v.bitPattern) } - mutating func writeDouble(_ v: Double) { writeUInt64(v.bitPattern) } - mutating func writeLen(_ n: Int) { - precondition(n >= 0 && n <= Int(UInt32.max), "WeaveFFI buffer length exceeds UInt32.max") - writeUInt32(UInt32(n)) - } - mutating func writeString(_ v: String) { - let utf8 = Array(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) } -} - -/// Traps on a malformed value buffer. Per the wire-format spec, consumers -/// surface decode failures through the same channel as a producer panic. -func wvDecodeFailure(_ context: String) -> Never { - fatalError("malformed WeaveFFI value buffer: \(context)") -} - -/// Deserializes values from the WeaveFFI value-buffer wire format, rejecting -/// truncated buffers, invalid flag bytes, oversized length prefixes, and -/// trailing bytes. -struct WvReader { - let bytes: [UInt8] - var pos: Int = 0 - - init(bytes: [UInt8]) { self.bytes = bytes } - - var remaining: Int { bytes.count - pos } - - mutating func take(_ n: Int, _ context: String) -> ArraySlice { - guard remaining >= n else { wvDecodeFailure(context) } - defer { pos += n } - return bytes[pos..<(pos + n)] - } - - mutating func readBool() -> Bool { - switch take(1, "bool").first! { - case 0: return false - case 1: return true - default: wvDecodeFailure("bool byte out of range") - } - } - mutating func readUInt8() -> UInt8 { take(1, "u8").first! } - mutating func readInt8() -> Int8 { Int8(bitPattern: readUInt8()) } - mutating func readUInt16() -> UInt16 { - var v: UInt16 = 0 - for (i, b) in take(2, "u16").enumerated() { v |= UInt16(b) << (8 * i) } - return v - } - mutating func readInt16() -> Int16 { Int16(bitPattern: readUInt16()) } - mutating func readUInt32() -> UInt32 { - var v: UInt32 = 0 - for (i, b) in take(4, "u32").enumerated() { v |= UInt32(b) << (8 * i) } - return v - } - mutating func readInt32() -> Int32 { Int32(bitPattern: readUInt32()) } - mutating func readUInt64() -> UInt64 { - var v: UInt64 = 0 - for (i, b) in take(8, "u64").enumerated() { v |= UInt64(b) << (8 * i) } - return v - } - mutating func readInt64() -> Int64 { Int64(bitPattern: readUInt64()) } - mutating func readFloat() -> Float { Float(bitPattern: readUInt32()) } - mutating func readDouble() -> Double { Double(bitPattern: readUInt64()) } - mutating func readLen() -> Int { - let n = Int(readUInt32()) - guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } - return n - } - mutating func readString() -> String { - let n = readLen() - guard let s = String(bytes: take(n, "string bytes"), encoding: .utf8) else { - wvDecodeFailure("string is not valid UTF-8") - } - return s - } - mutating func readBytes() -> Data { - let n = readLen() - return Data(take(n, "byte buffer")) - } - mutating func readOptionFlag() -> Bool { - switch take(1, "option flag").first! { - case 0: return false - case 1: return true - default: wvDecodeFailure("option flag byte out of range") - } - } - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -final class WvCallbackBox { - let value: T - init(_ value: T) { self.value = value } -} - -var wvListenerContexts: [UInt64: UnsafeMutableRawPointer] = [:] -let wvListenerLock = NSLock() - -/// A lazy sequence over the `String` elements streamed by `weaveffi_events_get_messages`. -/// -/// Each `next()` call pulls exactly one element from the producer. The -/// underlying C iterator is destroyed eagerly on exhaustion and from -/// `deinit` when iteration is abandoned early. -public final class EventsGetMessagesIterator: Sequence, IteratorProtocol { - private var handle: OpaquePointer? - - init(handle: OpaquePointer) { - self.handle = handle - } - - deinit { - destroyHandle() - } - - private func destroyHandle() { - guard let handle = handle else { return } - weaveffi_events_GetMessagesIterator_destroy(handle) - self.handle = nil - } - - /// Pulls the next element from the producer, 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 err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - if weaveffi_events_GetMessagesIterator_next(handle, &item, &err) == 0 { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - weaveffi_error_clear(&err) - fatalError("\(code): \(message)") - } - destroyHandle() - return nil - } - let element = String(cString: item!) - weaveffi_free_string(item) - return element - } -} - -public enum Events { - /// - Returns: A subscription id for ``unregisterMessageListener(_:)``. - public static func registerMessageListener(_ callback: @escaping (String) -> Void) -> UInt64 { - let box = WvCallbackBox(callback) - let ctx = Unmanaged.passRetained(box).toOpaque() - let id = weaveffi_events_register_message_listener({ message, context in - let cb = Unmanaged Void>>.fromOpaque(context!).takeUnretainedValue().value - cb(String(cString: message!)) - }, ctx) - wvListenerLock.lock() - wvListenerContexts[id] = ctx - wvListenerLock.unlock() - return id - } - /// Unregisters a listener previously registered with ``registerMessageListener(_:)``. - public static func unregisterMessageListener(_ id: UInt64) { - weaveffi_events_unregister_message_listener(id) - wvListenerLock.lock() - let ctx = wvListenerContexts.removeValue(forKey: id) - wvListenerLock.unlock() - if let ctx = ctx { - Unmanaged Void>>.fromOpaque(ctx).release() - } - } - /// Send a message, triggering the OnMessage callback - public static func sendMessage(text: String) -> Void { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - text.withCString { text_ptr in - weaveffi_events_send_message(text_ptr, &err) - } - trap(&err) - } - /// Return an iterator over all sent messages - /// - Returns: A lazy sequence that pulls one element per step from the - /// producer; the underlying iterator is destroyed when the sequence is - /// exhausted or deinitialized. - public static func getMessages() -> EventsGetMessagesIterator { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_events_get_messages(&err) - trap(&err) - guard let rv = rv else { fatalError("-1: null iterator") } - return EventsGetMessagesIterator(handle: rv) - } -} - -// END WeaveFFI.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_inventory__Package_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_inventory__Package_swift.snap deleted file mode 100644 index 7ec6e100..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_inventory__Package_swift.snap +++ /dev/null @@ -1,23 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// swift-tools-version:5.7 -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -import PackageDescription - -let package = Package( - name: "WeaveFFI", - platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], - products: [ - .library(name: "WeaveFFI", targets: ["WeaveFFI"]), - ], - targets: [ - .systemLibrary(name: "CWeaveFFI"), - .target(name: "WeaveFFI", dependencies: ["CWeaveFFI"]), - ] -) - -// END Package.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_inventory__Sources_CWeaveFFI_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_inventory__Sources_CWeaveFFI_module_modulemap.snap deleted file mode 100644 index e97f96e9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_inventory__Sources_CWeaveFFI_module_modulemap.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -module CWeaveFFI [system] { - header "../../../c/weaveffi.h" - link "weaveffi" - export * -} - -// END module.modulemap diff --git a/crates/weaveffi-cli/tests/snapshots/swift_inventory__Sources_WeaveFFI_WeaveFFI_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_inventory__Sources_WeaveFFI_WeaveFFI_swift.snap deleted file mode 100644 index 509d6238..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_inventory__Sources_WeaveFFI_WeaveFFI_swift.snap +++ /dev/null @@ -1,511 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -import CWeaveFFI -import Foundation - -public enum WeaveFFIError: Error, LocalizedError { - case error(code: Int32, message: String) - public var errorDescription: String? { - switch self { - case let .error(_, message): return message - } - } - public var errorCode: Int32 { - switch self { - case let .error(code, _): return code - } - } -} - -@inline(__always) -func check(_ err: inout weaveffi_error) throws { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - weaveffi_error_clear(&err) - throw WeaveFFIError.error(code: code, message: message) - } -} - -@inline(__always) -func trap(_ err: inout weaveffi_error) { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - weaveffi_error_clear(&err) - fatalError("\(code): \(message)") - } -} - -/// Serializes values into the WeaveFFI value-buffer wire format -/// (little-endian, packed, no alignment). -struct WvWriter { - var bytes: [UInt8] = [] - - mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } - mutating func writeInt8(_ v: Int8) { bytes.append(UInt8(bitPattern: v)) } - mutating func writeUInt8(_ v: UInt8) { bytes.append(v) } - mutating func writeUInt16(_ v: UInt16) { - bytes.append(UInt8(truncatingIfNeeded: v)) - bytes.append(UInt8(truncatingIfNeeded: v >> 8)) - } - mutating func writeInt16(_ v: Int16) { writeUInt16(UInt16(bitPattern: v)) } - mutating func writeUInt32(_ v: UInt32) { - bytes.append(UInt8(truncatingIfNeeded: v)) - bytes.append(UInt8(truncatingIfNeeded: v >> 8)) - bytes.append(UInt8(truncatingIfNeeded: v >> 16)) - bytes.append(UInt8(truncatingIfNeeded: v >> 24)) - } - mutating func writeInt32(_ v: Int32) { writeUInt32(UInt32(bitPattern: v)) } - mutating func writeUInt64(_ v: UInt64) { - writeUInt32(UInt32(truncatingIfNeeded: v)) - writeUInt32(UInt32(truncatingIfNeeded: v >> 32)) - } - mutating func writeInt64(_ v: Int64) { writeUInt64(UInt64(bitPattern: v)) } - mutating func writeFloat(_ v: Float) { writeUInt32(v.bitPattern) } - mutating func writeDouble(_ v: Double) { writeUInt64(v.bitPattern) } - mutating func writeLen(_ n: Int) { - precondition(n >= 0 && n <= Int(UInt32.max), "WeaveFFI buffer length exceeds UInt32.max") - writeUInt32(UInt32(n)) - } - mutating func writeString(_ v: String) { - let utf8 = Array(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) } -} - -/// Traps on a malformed value buffer. Per the wire-format spec, consumers -/// surface decode failures through the same channel as a producer panic. -func wvDecodeFailure(_ context: String) -> Never { - fatalError("malformed WeaveFFI value buffer: \(context)") -} - -/// Deserializes values from the WeaveFFI value-buffer wire format, rejecting -/// truncated buffers, invalid flag bytes, oversized length prefixes, and -/// trailing bytes. -struct WvReader { - let bytes: [UInt8] - var pos: Int = 0 - - init(bytes: [UInt8]) { self.bytes = bytes } - - var remaining: Int { bytes.count - pos } - - mutating func take(_ n: Int, _ context: String) -> ArraySlice { - guard remaining >= n else { wvDecodeFailure(context) } - defer { pos += n } - return bytes[pos..<(pos + n)] - } - - mutating func readBool() -> Bool { - switch take(1, "bool").first! { - case 0: return false - case 1: return true - default: wvDecodeFailure("bool byte out of range") - } - } - mutating func readUInt8() -> UInt8 { take(1, "u8").first! } - mutating func readInt8() -> Int8 { Int8(bitPattern: readUInt8()) } - mutating func readUInt16() -> UInt16 { - var v: UInt16 = 0 - for (i, b) in take(2, "u16").enumerated() { v |= UInt16(b) << (8 * i) } - return v - } - mutating func readInt16() -> Int16 { Int16(bitPattern: readUInt16()) } - mutating func readUInt32() -> UInt32 { - var v: UInt32 = 0 - for (i, b) in take(4, "u32").enumerated() { v |= UInt32(b) << (8 * i) } - return v - } - mutating func readInt32() -> Int32 { Int32(bitPattern: readUInt32()) } - mutating func readUInt64() -> UInt64 { - var v: UInt64 = 0 - for (i, b) in take(8, "u64").enumerated() { v |= UInt64(b) << (8 * i) } - return v - } - mutating func readInt64() -> Int64 { Int64(bitPattern: readUInt64()) } - mutating func readFloat() -> Float { Float(bitPattern: readUInt32()) } - mutating func readDouble() -> Double { Double(bitPattern: readUInt64()) } - mutating func readLen() -> Int { - let n = Int(readUInt32()) - guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } - return n - } - mutating func readString() -> String { - let n = readLen() - guard let s = String(bytes: take(n, "string bytes"), encoding: .utf8) else { - wvDecodeFailure("string is not valid UTF-8") - } - return s - } - mutating func readBytes() -> Data { - let n = readLen() - return Data(take(n, "byte buffer")) - } - mutating func readOptionFlag() -> Bool { - switch take(1, "option flag").first! { - case 0: return false - case 1: return true - default: wvDecodeFailure("option flag byte out of range") - } - } - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -/// Typed errors reported by the `products` module. -public enum ProductsError: Error, LocalizedError { - case invalidPrice(message: String) - case productNotFound(message: String) - public var errorDescription: String? { - switch self { - case let .invalidPrice(message): return message - case let .productNotFound(message): return message - } - } - /// The numeric ABI code carried by this error. - public var errorCode: Int32 { - switch self { - case .invalidPrice: return 1 - case .productNotFound: return 2 - } - } -} - -@inline(__always) -func mapProducts(code: Int32, message: String, payload: [UInt8]?) -> Error { - switch code { - case 1: return ProductsError.invalidPrice(message: message.isEmpty ? "price must be positive" : message) - case 2: return ProductsError.productNotFound(message: message.isEmpty ? "product not found" : message) - default: return WeaveFFIError.error(code: code, message: message) - } -} - -@inline(__always) -func checkProducts(_ err: inout weaveffi_error) throws { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - let payload: [UInt8]? = err.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.payload_len)) } - weaveffi_error_clear(&err) - throw mapProducts(code: code, message: message, payload: payload) - } -} - -public enum Category: UInt32 { - case electronics = 0 - case clothing = 1 - case food = 2 - case books = 3 -} - -public struct Product { - public var id: Int64 - public var name: String - public var description: String? - public var price: Double - public var category: Category - public var tags: [String] - - /// Creates a `Product` value. - public init(id: Int64, name: String, description: String?, price: Double, category: Category, tags: [String]) { - self.id = id - self.name = name - self.description = description - self.price = price - self.category = category - self.tags = tags - } -} - -/// Serializes a `Product` into the value-buffer wire format. -func wvWriteProduct(_ value: Product, into w: inout WvWriter) { - w.writeInt64(value.id) - w.writeString(value.name) - if let v0 = value.description { - w.writeOptionFlag(true) - w.writeString(v0) - } else { - w.writeOptionFlag(false) - } - w.writeDouble(value.price) - w.writeInt32(Int32(bitPattern: value.category.rawValue)) - w.writeLen(value.tags.count) - for v1 in value.tags { - w.writeString(v1) - } -} - -/// Deserializes a `Product` from the value-buffer wire format. -func wvReadProduct(_ r: inout WvReader) -> Product { - let v2 = r.readInt64() - let v3 = r.readString() - var v4: String? = nil - if r.readOptionFlag() { - let v5 = r.readString() - v4 = v5 - } - let v6 = r.readDouble() - let v7 = Category(rawValue: UInt32(bitPattern: r.readInt32()))! - let n9 = r.readLen() - var v8: [String] = [] - v8.reserveCapacity(n9) - for _ in 0.. Product { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv: UnsafePointer? = name.withCString { name_ptr in - return weaveffi_products_Catalog_add_product(ptr, name_ptr, price, weaveffi_products_Category(category.rawValue), &outLen, &err) - } - try checkProducts(&err) - guard let rv = rv else { throw WeaveFFIError.error(code: -1, message: "null buffer") } - let rvBytes = [UInt8](UnsafeBufferPointer(start: rv, count: outLen)) - weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen) - var rvReader = WvReader(bytes: rvBytes) - let v0 = wvReadProduct(&rvReader) - rvReader.finish() - return v0 - } - public func getProduct(id: Int64) throws -> Product { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_products_Catalog_get_product(ptr, id, &outLen, &err) - try checkProducts(&err) - guard let rv = rv else { throw WeaveFFIError.error(code: -1, message: "null buffer") } - let rvBytes = [UInt8](UnsafeBufferPointer(start: rv, count: outLen)) - weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen) - var rvReader = WvReader(bytes: rvBytes) - let v0 = wvReadProduct(&rvReader) - rvReader.finish() - return v0 - } - public func search(category: Category) -> [Product] { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_products_Catalog_search(ptr, weaveffi_products_Category(category.rawValue), &outLen, &err) - trap(&err) - guard let rv = rv else { fatalError("-1: null buffer") } - let rvBytes = [UInt8](UnsafeBufferPointer(start: rv, count: outLen)) - weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen) - var rvReader = WvReader(bytes: rvBytes) - let n1 = rvReader.readLen() - var v0: [Product] = [] - v0.reserveCapacity(n1) - for _ in 0.. Bool { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_products_Catalog_update_price(ptr, id, price, &err) - try checkProducts(&err) - return rv - } - public func remove(id: Int64) -> Bool { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_products_Catalog_remove(ptr, id, &err) - trap(&err) - return rv - } -} - -public enum Products { -} - -/// Typed errors reported by the `orders` module. -public enum OrdersError: Error, LocalizedError { - case orderNotFound(message: String) - case emptyOrder(message: String) - public var errorDescription: String? { - switch self { - case let .orderNotFound(message): return message - case let .emptyOrder(message): return message - } - } - /// The numeric ABI code carried by this error. - public var errorCode: Int32 { - switch self { - case .orderNotFound: return 1 - case .emptyOrder: return 2 - } - } -} - -@inline(__always) -func mapOrders(code: Int32, message: String, payload: [UInt8]?) -> Error { - switch code { - case 1: return OrdersError.orderNotFound(message: message.isEmpty ? "order not found" : message) - case 2: return OrdersError.emptyOrder(message: message.isEmpty ? "order must contain at least one item" : message) - default: return WeaveFFIError.error(code: code, message: message) - } -} - -@inline(__always) -func checkOrders(_ err: inout weaveffi_error) throws { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - let payload: [UInt8]? = err.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.payload_len)) } - weaveffi_error_clear(&err) - throw mapOrders(code: code, message: message, payload: payload) - } -} - -public struct OrderItem { - public var productId: Int64 - public var quantity: Int32 - public var unitPrice: Double - - /// Creates a `OrderItem` value. - public init(productId: Int64, quantity: Int32, unitPrice: Double) { - self.productId = productId - self.quantity = quantity - self.unitPrice = unitPrice - } -} - -/// Serializes a `OrderItem` into the value-buffer wire format. -func wvWriteOrderItem(_ value: OrderItem, into w: inout WvWriter) { - w.writeInt64(value.productId) - w.writeInt32(value.quantity) - w.writeDouble(value.unitPrice) -} - -/// Deserializes a `OrderItem` from the value-buffer wire format. -func wvReadOrderItem(_ r: inout WvReader) -> OrderItem { - let v0 = r.readInt64() - let v1 = r.readInt32() - let v2 = r.readDouble() - return OrderItem(productId: v0, quantity: v1, unitPrice: v2) -} - -public struct Order { - public var id: Int64 - public var items: [OrderItem] - public var total: Double - public var status: String - - /// Creates a `Order` value. - public init(id: Int64, items: [OrderItem], total: Double, status: String) { - self.id = id - self.items = items - self.total = total - self.status = status - } -} - -/// Serializes a `Order` into the value-buffer wire format. -func wvWriteOrder(_ value: Order, into w: inout WvWriter) { - w.writeInt64(value.id) - w.writeLen(value.items.count) - for v0 in value.items { - wvWriteOrderItem(v0, into: &w) - } - w.writeDouble(value.total) - w.writeString(value.status) -} - -/// Deserializes a `Order` from the value-buffer wire format. -func wvReadOrder(_ r: inout WvReader) -> Order { - let v1 = r.readInt64() - let n3 = r.readLen() - var v2: [OrderItem] = [] - v2.reserveCapacity(n3) - for _ in 0.. Int64 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var itemsWriter = WvWriter() - itemsWriter.writeLen(items.count) - for v0 in items { - wvWriteOrderItem(v0, into: &itemsWriter) - } - let rv: Int64 = itemsWriter.bytes.withUnsafeBufferPointer { items_buf in - let items_ptr = items_buf.baseAddress - let items_len = items_buf.count - return weaveffi_orders_create_order(items_ptr, items_len, &err) - } - try checkOrders(&err) - return rv - } - public static func getOrder(id: Int64) throws -> Order { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_orders_get_order(id, &outLen, &err) - try checkOrders(&err) - guard let rv = rv else { throw WeaveFFIError.error(code: -1, message: "null buffer") } - let rvBytes = [UInt8](UnsafeBufferPointer(start: rv, count: outLen)) - weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen) - var rvReader = WvReader(bytes: rvBytes) - let v0 = wvReadOrder(&rvReader) - rvReader.finish() - return v0 - } - public static func cancelOrder(id: Int64) -> Bool { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_orders_cancel_order(id, &err) - trap(&err) - return rv - } - public static func addProductToOrder(orderId: Int64, product: Product) -> Bool { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var productWriter = WvWriter() - wvWriteProduct(product, into: &productWriter) - let rv: Bool = productWriter.bytes.withUnsafeBufferPointer { product_buf in - let product_ptr = product_buf.baseAddress - let product_len = product_buf.count - return weaveffi_orders_add_product_to_order(orderId, product_ptr, product_len, &err) - } - trap(&err) - return rv - } -} - -// END WeaveFFI.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_kvstore__Package_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_kvstore__Package_swift.snap deleted file mode 100644 index 7ec6e100..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_kvstore__Package_swift.snap +++ /dev/null @@ -1,23 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// swift-tools-version:5.7 -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -import PackageDescription - -let package = Package( - name: "WeaveFFI", - platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], - products: [ - .library(name: "WeaveFFI", targets: ["WeaveFFI"]), - ], - targets: [ - .systemLibrary(name: "CWeaveFFI"), - .target(name: "WeaveFFI", dependencies: ["CWeaveFFI"]), - ] -) - -// END Package.swift diff --git a/crates/weaveffi-cli/tests/snapshots/swift_kvstore__Sources_CWeaveFFI_module_modulemap.snap b/crates/weaveffi-cli/tests/snapshots/swift_kvstore__Sources_CWeaveFFI_module_modulemap.snap deleted file mode 100644 index e97f96e9..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_kvstore__Sources_CWeaveFFI_module_modulemap.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -module CWeaveFFI [system] { - header "../../../c/weaveffi.h" - link "weaveffi" - export * -} - -// END module.modulemap diff --git a/crates/weaveffi-cli/tests/snapshots/swift_kvstore__Sources_WeaveFFI_WeaveFFI_swift.snap b/crates/weaveffi-cli/tests/snapshots/swift_kvstore__Sources_WeaveFFI_WeaveFFI_swift.snap deleted file mode 100644 index 21756547..00000000 --- a/crates/weaveffi-cli/tests/snapshots/swift_kvstore__Sources_WeaveFFI_WeaveFFI_swift.snap +++ /dev/null @@ -1,603 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -import CWeaveFFI -import Foundation - -public enum WeaveFFIError: Error, LocalizedError { - case error(code: Int32, message: String) - public var errorDescription: String? { - switch self { - case let .error(_, message): return message - } - } - public var errorCode: Int32 { - switch self { - case let .error(code, _): return code - } - } -} - -@inline(__always) -func check(_ err: inout weaveffi_error) throws { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - weaveffi_error_clear(&err) - throw WeaveFFIError.error(code: code, message: message) - } -} - -@inline(__always) -func trap(_ err: inout weaveffi_error) { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - weaveffi_error_clear(&err) - fatalError("\(code): \(message)") - } -} - -/// Serializes values into the WeaveFFI value-buffer wire format -/// (little-endian, packed, no alignment). -struct WvWriter { - var bytes: [UInt8] = [] - - mutating func writeBool(_ v: Bool) { bytes.append(v ? 1 : 0) } - mutating func writeInt8(_ v: Int8) { bytes.append(UInt8(bitPattern: v)) } - mutating func writeUInt8(_ v: UInt8) { bytes.append(v) } - mutating func writeUInt16(_ v: UInt16) { - bytes.append(UInt8(truncatingIfNeeded: v)) - bytes.append(UInt8(truncatingIfNeeded: v >> 8)) - } - mutating func writeInt16(_ v: Int16) { writeUInt16(UInt16(bitPattern: v)) } - mutating func writeUInt32(_ v: UInt32) { - bytes.append(UInt8(truncatingIfNeeded: v)) - bytes.append(UInt8(truncatingIfNeeded: v >> 8)) - bytes.append(UInt8(truncatingIfNeeded: v >> 16)) - bytes.append(UInt8(truncatingIfNeeded: v >> 24)) - } - mutating func writeInt32(_ v: Int32) { writeUInt32(UInt32(bitPattern: v)) } - mutating func writeUInt64(_ v: UInt64) { - writeUInt32(UInt32(truncatingIfNeeded: v)) - writeUInt32(UInt32(truncatingIfNeeded: v >> 32)) - } - mutating func writeInt64(_ v: Int64) { writeUInt64(UInt64(bitPattern: v)) } - mutating func writeFloat(_ v: Float) { writeUInt32(v.bitPattern) } - mutating func writeDouble(_ v: Double) { writeUInt64(v.bitPattern) } - mutating func writeLen(_ n: Int) { - precondition(n >= 0 && n <= Int(UInt32.max), "WeaveFFI buffer length exceeds UInt32.max") - writeUInt32(UInt32(n)) - } - mutating func writeString(_ v: String) { - let utf8 = Array(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) } -} - -/// Traps on a malformed value buffer. Per the wire-format spec, consumers -/// surface decode failures through the same channel as a producer panic. -func wvDecodeFailure(_ context: String) -> Never { - fatalError("malformed WeaveFFI value buffer: \(context)") -} - -/// Deserializes values from the WeaveFFI value-buffer wire format, rejecting -/// truncated buffers, invalid flag bytes, oversized length prefixes, and -/// trailing bytes. -struct WvReader { - let bytes: [UInt8] - var pos: Int = 0 - - init(bytes: [UInt8]) { self.bytes = bytes } - - var remaining: Int { bytes.count - pos } - - mutating func take(_ n: Int, _ context: String) -> ArraySlice { - guard remaining >= n else { wvDecodeFailure(context) } - defer { pos += n } - return bytes[pos..<(pos + n)] - } - - mutating func readBool() -> Bool { - switch take(1, "bool").first! { - case 0: return false - case 1: return true - default: wvDecodeFailure("bool byte out of range") - } - } - mutating func readUInt8() -> UInt8 { take(1, "u8").first! } - mutating func readInt8() -> Int8 { Int8(bitPattern: readUInt8()) } - mutating func readUInt16() -> UInt16 { - var v: UInt16 = 0 - for (i, b) in take(2, "u16").enumerated() { v |= UInt16(b) << (8 * i) } - return v - } - mutating func readInt16() -> Int16 { Int16(bitPattern: readUInt16()) } - mutating func readUInt32() -> UInt32 { - var v: UInt32 = 0 - for (i, b) in take(4, "u32").enumerated() { v |= UInt32(b) << (8 * i) } - return v - } - mutating func readInt32() -> Int32 { Int32(bitPattern: readUInt32()) } - mutating func readUInt64() -> UInt64 { - var v: UInt64 = 0 - for (i, b) in take(8, "u64").enumerated() { v |= UInt64(b) << (8 * i) } - return v - } - mutating func readInt64() -> Int64 { Int64(bitPattern: readUInt64()) } - mutating func readFloat() -> Float { Float(bitPattern: readUInt32()) } - mutating func readDouble() -> Double { Double(bitPattern: readUInt64()) } - mutating func readLen() -> Int { - let n = Int(readUInt32()) - guard n <= remaining else { wvDecodeFailure("length prefix exceeds remaining buffer") } - return n - } - mutating func readString() -> String { - let n = readLen() - guard let s = String(bytes: take(n, "string bytes"), encoding: .utf8) else { - wvDecodeFailure("string is not valid UTF-8") - } - return s - } - mutating func readBytes() -> Data { - let n = readLen() - return Data(take(n, "byte buffer")) - } - mutating func readOptionFlag() -> Bool { - switch take(1, "option flag").first! { - case 0: return false - case 1: return true - default: wvDecodeFailure("option flag byte out of range") - } - } - func finish() { - if remaining != 0 { wvDecodeFailure("trailing bytes after value") } - } -} - -private final class ContinuationRef { - let value: CheckedContinuation - init(_ value: CheckedContinuation) { self.value = value } -} - -final class WvCallbackBox { - let value: T - init(_ value: T) { self.value = value } -} - -var wvListenerContexts: [UInt64: UnsafeMutableRawPointer] = [:] -let wvListenerLock = NSLock() - -/// Typed errors reported by the `kv` module. -public enum KvError: Error, LocalizedError { - /// No entry exists for the requested key - case keyNotFound(message: String) - /// The entry exists but its TTL has elapsed - case expired(message: String) - /// The store has reached its capacity limit - case storeFull(message: String) - /// Underlying storage reported an I/O failure - case ioError(message: String) - public var errorDescription: String? { - switch self { - case let .keyNotFound(message): return message - case let .expired(message): return message - case let .storeFull(message): return message - case let .ioError(message): return message - } - } - /// The numeric ABI code carried by this error. - public var errorCode: Int32 { - switch self { - case .keyNotFound: return 1001 - case .expired: return 1002 - case .storeFull: return 1003 - case .ioError: return 1004 - } - } -} - -@inline(__always) -func mapKv(code: Int32, message: String, payload: [UInt8]?) -> Error { - switch code { - case 1001: return KvError.keyNotFound(message: message.isEmpty ? "key not found" : message) - case 1002: return KvError.expired(message: message.isEmpty ? "entry expired" : message) - case 1003: return KvError.storeFull(message: message.isEmpty ? "store has reached capacity" : message) - case 1004: return KvError.ioError(message: message.isEmpty ? "I/O failure" : message) - default: return WeaveFFIError.error(code: code, message: message) - } -} - -@inline(__always) -func checkKv(_ err: inout weaveffi_error) throws { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - let payload: [UInt8]? = err.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.payload_len)) } - weaveffi_error_clear(&err) - throw mapKv(code: code, message: message, payload: payload) - } -} - -/// Persistence semantics applied to a stored entry -public enum EntryKind: UInt32 { - /// In-memory only; lost on close - case volatile = 0 - /// Flushed to durable storage - case persistent = 1 - /// Persistent and encrypted at rest - case encrypted = 2 -} - -/// A single key-value entry persisted in the store -public struct Entry { - /// Stable monotonic identifier assigned on insert - public var id: Int64 - /// UTF-8 lookup key - public var key: String - /// Opaque binary payload - public var value: Data - /// Unix-timestamp seconds when the entry was created - public var createdAt: Int64 - /// Optional unix-timestamp seconds at which the entry expires - public var expiresAt: Int64? - /// Free-form labels attached to the entry - public var tags: [String] - /// Arbitrary string-valued metadata pairs - public var metadata: [String: String] - - /// Creates a `Entry` value. - public init(id: Int64, key: String, value: Data, createdAt: Int64, expiresAt: Int64?, tags: [String], metadata: [String: String]) { - self.id = id - self.key = key - self.value = value - self.createdAt = createdAt - self.expiresAt = expiresAt - self.tags = tags - self.metadata = metadata - } -} - -/// Serializes a `Entry` into the value-buffer wire format. -func wvWriteEntry(_ value: Entry, into w: inout WvWriter) { - w.writeInt64(value.id) - w.writeString(value.key) - w.writeBytes(value.value) - w.writeInt64(value.createdAt) - if let v0 = value.expiresAt { - w.writeOptionFlag(true) - w.writeInt64(v0) - } else { - w.writeOptionFlag(false) - } - w.writeLen(value.tags.count) - for v1 in value.tags { - w.writeString(v1) - } - w.writeLen(value.metadata.count) - for (v2, v3) in value.metadata { - w.writeString(v2) - w.writeString(v3) - } -} - -/// Deserializes a `Entry` from the value-buffer wire format. -func wvReadEntry(_ r: inout WvReader) -> Entry { - let v4 = r.readInt64() - let v5 = r.readString() - let v6 = r.readBytes() - let v7 = r.readInt64() - var v8: Int64? = nil - if r.readOptionFlag() { - let v9 = r.readInt64() - v8 = v9 - } - let n11 = r.readLen() - var v10: [String] = [] - v10.reserveCapacity(n11) - for _ in 0.. Store { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv: OpaquePointer? = path.withCString { path_ptr in - return weaveffi_kv_Store_open(path_ptr, &err) - } - try checkKv(&err) - guard let rv = rv else { throw WeaveFFIError.error(code: -1, message: "null pointer") } - return Store(ptr: rv) - } - /// Insert or replace a value, returning true on success - public func put(key: String, value: Data, kind: EntryKind, ttlSeconds: Int64?) throws -> Bool { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let valueBytes = Array(value) - var ttlSecondsWriter = WvWriter() - if let v0 = ttlSeconds { - ttlSecondsWriter.writeOptionFlag(true) - ttlSecondsWriter.writeInt64(v0) - } else { - ttlSecondsWriter.writeOptionFlag(false) - } - let rv: Bool = key.withCString { key_ptr in - return valueBytes.withUnsafeBufferPointer { value_buf in - let value_ptr = value_buf.baseAddress - let value_len = value_buf.count - return ttlSecondsWriter.bytes.withUnsafeBufferPointer { ttlSeconds_buf in - let ttlSeconds_ptr = ttlSeconds_buf.baseAddress - let ttlSeconds_len = ttlSeconds_buf.count - return weaveffi_kv_Store_put(ptr, key_ptr, value_ptr, value_len, weaveffi_kv_EntryKind(kind.rawValue), ttlSeconds_ptr, ttlSeconds_len, &err) - } - } - } - try checkKv(&err) - return rv - } - /// Look up an entry by key; returns null if missing or expired - public func get(key: String) throws -> Entry? { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv: UnsafePointer? = key.withCString { key_ptr in - return weaveffi_kv_Store_get(ptr, key_ptr, &outLen, &err) - } - try checkKv(&err) - guard let rv = rv else { throw WeaveFFIError.error(code: -1, message: "null buffer") } - let rvBytes = [UInt8](UnsafeBufferPointer(start: rv, count: outLen)) - weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen) - var rvReader = WvReader(bytes: rvBytes) - var v0: Entry? = nil - if rvReader.readOptionFlag() { - let v1 = wvReadEntry(&rvReader) - v0 = v1 - } - rvReader.finish() - return v0 - } - /// Remove the entry for the given key, returning true if it existed - public func delete(key: String) throws -> Bool { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv: Bool = key.withCString { key_ptr in - return weaveffi_kv_Store_delete(ptr, key_ptr, &err) - } - try checkKv(&err) - return rv - } - /// Stream every key, optionally filtered by a prefix - /// - Returns: A lazy sequence that pulls one element per step from the - /// producer; the underlying iterator is destroyed when the sequence is - /// exhausted or deinitialized. - /// - Throws: The module's typed error if the launch fails. Mid-stream - /// errors end iteration and are stored in the sequence's `error` - /// property instead of being thrown. - public func listKeys(prefix: String?) throws -> KvStoreListKeysIterator { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var prefixWriter = WvWriter() - if let v0 = prefix { - prefixWriter.writeOptionFlag(true) - prefixWriter.writeString(v0) - } else { - prefixWriter.writeOptionFlag(false) - } - let rv: OpaquePointer? = prefixWriter.bytes.withUnsafeBufferPointer { prefix_buf in - let prefix_ptr = prefix_buf.baseAddress - let prefix_len = prefix_buf.count - return weaveffi_kv_Store_list_keys(ptr, prefix_ptr, prefix_len, &err) - } - try checkKv(&err) - guard let rv = rv else { throw WeaveFFIError.error(code: -1, message: "null iterator") } - return KvStoreListKeysIterator(handle: rv) - } - /// Return the number of live entries in the store - public func count() -> Int64 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kv_Store_count(ptr, &err) - trap(&err) - return rv - } - /// Drop every entry from the store - public func clear() -> Void { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - weaveffi_kv_Store_clear(ptr, &err) - trap(&err) - } - /// Reclaim space asynchronously; returns the number of bytes reclaimed - public func compact() async throws -> Int64 { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - let ctx = Unmanaged.passRetained(ContinuationRef(continuation)).toOpaque() - weaveffi_kv_Store_compact_async(ptr, nil, { context, err, result in - let contRef = Unmanaged>.fromOpaque(context!).takeRetainedValue() - if let err = err, err.pointee.code != 0 { - let code = err.pointee.code - let msg = err.pointee.message.flatMap { String(cString: $0) } ?? "" - let payload: [UInt8]? = err.pointee.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.pointee.payload_len)) } - weaveffi_error_free(err) - contRef.value.resume(throwing: mapKv(code: code, message: msg, payload: payload)) - } else { - contRef.value.resume(returning: result) - } - }, ctx) - } - } - /// Legacy single-shot put kept for compatibility - @available(*, deprecated, message: "use put() with explicit kind") - public func legacyPut(key: String, value: Data) throws -> Bool { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let valueBytes = Array(value) - let rv: Bool = key.withCString { key_ptr in - return valueBytes.withUnsafeBufferPointer { value_buf in - let value_ptr = value_buf.baseAddress - let value_len = value_buf.count - return weaveffi_kv_Store_legacy_put(ptr, key_ptr, value_ptr, value_len, &err) - } - } - try checkKv(&err) - return rv - } - /// The largest number of live entries one store will hold - public static func defaultCapacity() -> Int64 { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - let rv = weaveffi_kv_Store_default_capacity(&err) - trap(&err) - return rv - } -} - -/// A lazy sequence over the `String` elements streamed by `weaveffi_kv_Store_list_keys`. -/// -/// Each `next()` call pulls exactly one element from the producer. The -/// underlying C iterator is destroyed eagerly on exhaustion and from -/// `deinit` when iteration is abandoned early. -/// -/// If the producer reports an error mid-stream, iteration ends and the -/// error is stored in ``error`` for the caller to inspect after the loop. -public final class KvStoreListKeysIterator: Sequence, IteratorProtocol { - private var handle: OpaquePointer? - /// The error that ended iteration early, if any. - public private(set) var error: Error? - - init(handle: OpaquePointer) { - self.handle = handle - } - - deinit { - destroyHandle() - } - - private func destroyHandle() { - guard let handle = handle else { return } - weaveffi_kv_Store_ListKeysIterator_destroy(handle) - self.handle = nil - } - - /// Pulls the next element from the producer, 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 err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - if weaveffi_kv_Store_ListKeysIterator_next(handle, &item, &err) == 0 { - if err.code != 0 { - let code = err.code - let message = err.message.flatMap { String(cString: $0) } ?? "" - let payload: [UInt8]? = err.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.payload_len)) } - weaveffi_error_clear(&err) - self.error = mapKv(code: code, message: message, payload: payload) - } - destroyHandle() - return nil - } - let element = String(cString: item!) - weaveffi_free_string(item) - return element - } -} - -/// Aggregate store statistics -public struct Stats { - /// Number of live entries - public var totalEntries: Int64 - /// Sum of all value byte lengths - public var totalBytes: Int64 - /// Number of entries past their TTL but not yet evicted - public var expiredEntries: Int64 - - /// Creates a `Stats` value. - public init(totalEntries: Int64, totalBytes: Int64, expiredEntries: Int64) { - self.totalEntries = totalEntries - self.totalBytes = totalBytes - self.expiredEntries = expiredEntries - } -} - -/// Serializes a `Stats` into the value-buffer wire format. -func wvWriteStats(_ value: WeaveFFI.Stats, into w: inout WvWriter) { - w.writeInt64(value.totalEntries) - w.writeInt64(value.totalBytes) - w.writeInt64(value.expiredEntries) -} - -/// Deserializes a `Stats` from the value-buffer wire format. -func wvReadStats(_ r: inout WvReader) -> WeaveFFI.Stats { - let v0 = r.readInt64() - let v1 = r.readInt64() - let v2 = r.readInt64() - return WeaveFFI.Stats(totalEntries: v0, totalBytes: v1, expiredEntries: v2) -} - -public enum Kv { - /// Subscribe to per-key eviction notifications - /// - Returns: A subscription id for ``unregisterEvictionListener(_:)``. - public static func registerEvictionListener(_ callback: @escaping (String) -> Void) -> UInt64 { - let box = WvCallbackBox(callback) - let ctx = Unmanaged.passRetained(box).toOpaque() - let id = weaveffi_kv_register_eviction_listener({ key, context in - let cb = Unmanaged Void>>.fromOpaque(context!).takeUnretainedValue().value - cb(String(cString: key!)) - }, ctx) - wvListenerLock.lock() - wvListenerContexts[id] = ctx - wvListenerLock.unlock() - return id - } - /// Unregisters a listener previously registered with ``registerEvictionListener(_:)``. - public static func unregisterEvictionListener(_ id: UInt64) { - weaveffi_kv_unregister_eviction_listener(id) - wvListenerLock.lock() - let ctx = wvListenerContexts.removeValue(forKey: id) - wvListenerLock.unlock() - if let ctx = ctx { - Unmanaged Void>>.fromOpaque(ctx).release() - } - } - public enum Stats { - /// Snapshot the current store statistics - public static func getStats(store: Store) throws -> WeaveFFI.Stats { - var err = weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0) - var outLen: Int = 0 - let rv = weaveffi_kv_stats_get_stats(store.ptr, &outLen, &err) - try checkKv(&err) - guard let rv = rv else { throw WeaveFFIError.error(code: -1, message: "null buffer") } - let rvBytes = [UInt8](UnsafeBufferPointer(start: rv, count: outLen)) - weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen) - var rvReader = WvReader(bytes: rvBytes) - let v0 = wvReadStats(&rvReader) - rvReader.finish() - return v0 - } - } -} - -// END WeaveFFI.swift diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_async_demo__README_md.snap b/crates/weaveffi-cli/tests/snapshots/wasm_async_demo__README_md.snap deleted file mode 100644 index 6e373f29..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_async_demo__README_md.snap +++ /dev/null @@ -1,135 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI Wasm (experimental) - -This folder contains a minimal stub to help you load a `wasm32-unknown-unknown` build of your WeaveFFI library. - -Build (example): - -```bash -cargo build --target wasm32-unknown-unknown --release -``` - -Then serve the `.wasm` and use `weaveffi_wasm.js` to load it. - -## Complex Type Handling - -Wasm only supports numeric types natively (`i32`, `i64`, `f32`, `f64`). Complex types are encoded at the boundary as follows: - -### Records - -Records are **plain JavaScript objects**. A record crosses the boundary serialized in the WeaveFFI value-buffer format as one pointer + length pair in linear memory; the glue packs and unpacks it automatically, so there are no handles, no accessor functions, and nothing to free. - -### Enums - -C-style enums are passed as **`i32` values** corresponding to the variant's integer discriminant. Rich (algebraic) enums are plain objects tagged by variant name (`{ tag: "Circle", radius: 2 }`) and cross the boundary serialized in a value buffer, exactly like records. - -### Optionals - -Optional values map to **`null`** for the absent case. An optional crosses the boundary inside a value buffer as a one-byte presence flag followed by the value when present. The one exception is an optional interface, which stays a nullable object pointer (`0` = absent). - -### Lists and Maps - -Lists are JS arrays; maps are plain objects (a `Map` instance is also accepted on input). Both cross the boundary serialized in a value buffer as one pointer + length pair, recursing through element types. - -### Iterators - -`iter` functions return a **lazy JS iterator** (typed `IterableIterator`): each `next()` issues exactly one producer call, so iteration streams in constant memory. The producer handle is destroyed exactly once, on exhaustion or via `return()` when iteration stops early (a `for...of` loop calls `return()` automatically on `break` or `throw`). Abandoning an iterator without exhausting or closing it leaks the handle. - -### Error Handling - -The generated JS wrappers automatically handle errors by passing an error -pointer as the last argument to each Wasm function. The error struct is -16 bytes on wasm32: `{ i32 code, char* message, uint8_t* payload_ptr, -size_t payload_len }`. Your Wasm module must export the following -functions: - -- `weaveffi_alloc(size: i32) -> i32`: allocate `size` bytes in linear memory -- `weaveffi_dealloc(ptr: i32, size: i32)`: release a `weaveffi_alloc` block -- `weaveffi_error_clear(err_ptr: i32)`: clear and free error resources -- `weaveffi_free_string(ptr: i32)`: free a producer-returned C string -- `weaveffi_free_bytes(ptr: i32, len: i32)`: free a producer-returned buffer - -Wrappers of functions declared `throws` raise the declaring module's typed -error class (a `WeaveFFIError` subclass with a per-code subclass, such as -`KeyNotFound`); every other wrapper raises the generic `WeaveFFIError` only -for producer panics and marshalling failures. When the matched error code -declares payload fields, the wrapper decodes them from the error's value -buffer and attaches them as properties on the thrown error. - -## API Reference - -### Module: `tasks` - -#### Error Domain: `TaskError` - -Throwing wrappers in this module raise `TaskError` (a `WeaveFFIError` subclass); each code below is its own subclass carrying the stable `code`. Payload fields are decoded from the error's value buffer and attached as properties on the thrown error. - -| Class | Code | Default Message | Payload Fields | -|-------|------|-----------------|----------------| -| `InvalidName` | `1` | task name must not be empty | (none) | - -#### Functions - -##### `weaveffi_tasks_run_task` - -`weaveffi_tasks_run_task(name: i32) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `name` | `string` | `i32` | NUL-terminated C string pointer | -| _returns_ | `TaskResult` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_tasks_run_batch` - -`weaveffi_tasks_run_batch(names: i32, i32) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `names` | `[string]` | `i32, i32` | value buffer: ptr + len in linear memory | -| _returns_ | `[TaskResult]` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_tasks_cancel_task` - -`weaveffi_tasks_cancel_task(id: i64) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `id` | `i64` | `i64` | native Wasm i64 | -| _returns_ | `bool` | `i32` | 0 = false, 1 = true | - -##### `weaveffi_tasks_run_n_tasks` - -`weaveffi_tasks_run_n_tasks(n: i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `n` | `i32` | `i32` | native Wasm i32 | -| _returns_ | `i32` | `i32` | native Wasm i32 | - -##### `weaveffi_tasks_active_callbacks` - -`weaveffi_tasks_active_callbacks() -> i64` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `i64` | `i64` | native Wasm i64 | - -#### Structs - -##### `TaskResult` - -A plain JS object, serialized in a value buffer (`i32` ptr + `i32` len) at the boundary. Fields in declaration (wire) order: - -| Field | Type | -|-------|------| -| `id` | `i64` | -| `value` | `string` | -| `success` | `bool` | - - diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_async_demo__weaveffi_wasm_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_async_demo__weaveffi_wasm_d_ts.snap deleted file mode 100644 index f870ff54..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_async_demo__weaveffi_wasm_d_ts.snap +++ /dev/null @@ -1,63 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// Generated TypeScript declarations for WeaveFFI Wasm bindings - -/** Base error for WeaveFFI failures: domain errors extend it, and it is - * thrown directly for unknown codes, marshalling failures, and producer - * panics. Carries the stable ABI `code`. */ -export declare class WeaveFFIError extends Error { - constructor(code: number, message?: string); - code: number; -} - -/** Base error for the `tasks` module's error domain. */ -export declare class TaskError extends WeaveFFIError { - static readonly InvalidName: typeof InvalidName; -} - -/** task name must not be empty */ -export declare class InvalidName extends TaskError { - constructor(message?: string); - static readonly CODE: 1; -} - -export interface TaskResult { - id: bigint; - value: string; - success: boolean; -} - -export interface WeaveffiWasmModule { - _raw: WebAssembly.Exports; - tasks: { - /** - * @throws {TaskError} on a domain error code - */ - runTask(name: string): Promise; - /** - * @throws {WeaveFFIError} if the native call fails - */ - runBatch(names: string[]): Promise; - /** - * @throws {WeaveFFIError} if the native call fails - */ - cancelTask(id: bigint): boolean; - /** - * @throws {WeaveFFIError} if the native call fails - */ - runNTasks(n: number): Promise; - /** - * @throws {WeaveFFIError} if the native call fails - */ - activeCallbacks(): bigint; - }; -} - -export function loadWeaveffiWasm(url: string): Promise; - -// END weaveffi_wasm.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_async_demo__weaveffi_wasm_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_async_demo__weaveffi_wasm_js.snap deleted file mode 100644 index fb851343..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_async_demo__weaveffi_wasm_js.snap +++ /dev/null @@ -1,420 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// WeaveFFI Wasm bindings (auto-generated) -// -// Boundary conventions for a wasm32-unknown-unknown build: -// -// Objects -> i32 pointer into linear memory (0 = null/absent) -// Enums -> i32 discriminant value -// i64/u64 -> JavaScript BigInt -// Strings -> NUL-terminated UTF-8 (const char*); a single i32 pointer -// Bytes -> i32 data pointer + i32 length (out_len for returns) -// Buffered -> records, rich enums, optionals, lists, and maps cross -// as one value buffer: i32 pointer + i32 length - -/** Base error for WeaveFFI failures: domain errors extend it, and it is - * thrown directly for unknown codes, marshalling failures, and producer - * panics. Carries the stable ABI `code`. */ -export class WeaveFFIError extends Error { - constructor(code, message) { - super(message ? `WeaveFFI error ${code}: ${message}` : `WeaveFFI error ${code}`); - this.name = new.target.name; - this.code = code; - } -} - -/** Base error for the `tasks` module's error domain. */ -export class TaskError extends WeaveFFIError {} - -// task name must not be empty -export class InvalidName extends TaskError { - constructor(message = "task name must not be empty") { - super(1, message); - } -} -InvalidName.CODE = 1; -TaskError.InvalidName = InvalidName; - -const _TASK_ERROR_CODES = Object.freeze({ - 1: InvalidName, -}); - -// Build the TaskError subclass matching `code`, or a generic -// WeaveFFIError for codes outside the domain (panics, marshalling). -function _taskErrorFrom(wasm, code, message, payloadPtr, payloadLen) { - const _cls = _TASK_ERROR_CODES[code]; - const _e = _cls ? (message ? new _cls(message) : new _cls()) : new WeaveFFIError(code, message); - return _e; -} - -const _enc = new TextEncoder(); -const _dec = new TextDecoder(); - -// Stage a JS string as a NUL-terminated C string in linear memory. -// Returns [ptr, size] (size includes the NUL); release with _free. -function _cstr(wasm, str) { - const bytes = _enc.encode(str); - const size = bytes.length + 1; - const ptr = wasm.weaveffi_alloc(size); - const mem = new Uint8Array(wasm.memory.buffer, ptr, size); - mem.set(bytes); - mem[bytes.length] = 0; - return [ptr, size]; -} - -// Read a NUL-terminated C string (0 => null). Does not free. -function _readCStr(wasm, ptr) { - if (ptr === 0) return null; - const mem = new Uint8Array(wasm.memory.buffer); - let end = ptr; - while (mem[end] !== 0) end++; - return _dec.decode(mem.subarray(ptr, end)); -} - -// Read then free a producer-owned C string. -function _takeCStr(wasm, ptr) { - const s = _readCStr(wasm, ptr); - if (ptr !== 0) wasm.weaveffi_free_string(ptr); - return s; -} - -// Stage a byte buffer (or an encoded value buffer); returns [ptr, len]; -// release with weaveffi_dealloc(ptr, len). -function _bytes(wasm, data) { - const u8 = data instanceof Uint8Array ? data : new Uint8Array(data); - const ptr = wasm.weaveffi_alloc(u8.length); - if (u8.length) new Uint8Array(wasm.memory.buffer, ptr, u8.length).set(u8); - return [ptr, u8.length]; -} - -// Copy then free a producer-owned byte (or value) buffer. -function _takeBytes(wasm, ptr, len) { - if (ptr === 0 || len === 0) return new Uint8Array(0); - const copy = new Uint8Array(wasm.memory.buffer, ptr, len).slice(); - wasm.weaveffi_free_bytes(ptr, len); - return copy; -} - -// Growable little-endian byte writer implementing the WeaveFFI value-buffer -// wire format: values are packed back to back with no alignment; lengths and -// counts are u32; strings are u32 byte length + UTF-8 bytes (no NUL). -class _BufWriter { - constructor() { - this._u8 = new Uint8Array(64); - this._dv = new DataView(this._u8.buffer); - this._len = 0; - } - _need(n) { - if (this._len + n <= this._u8.length) return; - let cap = this._u8.length * 2; - while (cap < this._len + n) cap *= 2; - const u8 = new Uint8Array(cap); - u8.set(this._u8.subarray(0, this._len)); - this._u8 = u8; - this._dv = new DataView(u8.buffer); - } - bool(v) { this._need(1); this._dv.setUint8(this._len, v ? 1 : 0); this._len += 1; } - i8(v) { this._need(1); this._dv.setInt8(this._len, v); this._len += 1; } - u8(v) { this._need(1); this._dv.setUint8(this._len, v); this._len += 1; } - i16(v) { this._need(2); this._dv.setInt16(this._len, v, true); this._len += 2; } - u16(v) { this._need(2); this._dv.setUint16(this._len, v, true); this._len += 2; } - i32(v) { this._need(4); this._dv.setInt32(this._len, v, true); this._len += 4; } - u32(v) { this._need(4); this._dv.setUint32(this._len, v, true); this._len += 4; } - i64(v) { this._need(8); this._dv.setBigInt64(this._len, BigInt(v), true); this._len += 8; } - u64(v) { this._need(8); this._dv.setBigUint64(this._len, BigInt(v), true); this._len += 8; } - f32(v) { this._need(4); this._dv.setFloat32(this._len, v, true); this._len += 4; } - f64(v) { this._need(8); this._dv.setFloat64(this._len, v, true); this._len += 8; } - len(n) { this.u32(n); } - flag(present) { this.u8(present ? 1 : 0); } - str(v) { - const b = _enc.encode(v); - this.len(b.length); - this._need(b.length); - this._u8.set(b, this._len); - this._len += b.length; - } - bytes(v) { - const b = v instanceof Uint8Array ? v : new Uint8Array(v); - this.len(b.length); - this._need(b.length); - this._u8.set(b, this._len); - this._len += b.length; - } - finish() { return this._u8.subarray(0, this._len); } -} - -const _bufDec = new TextDecoder('utf-8', { fatal: true }); - -// Strict little-endian reader for the WeaveFFI value-buffer wire format. A -// malformed buffer (truncation, invalid bool or flag bytes, an oversized -// length prefix, invalid UTF-8, trailing bytes) is a producer/consumer -// contract violation and throws the generic brand error; code -3 marks a -// consumer-side marshalling failure. -class _BufReader { - constructor(bytes) { - this._u8 = bytes; - this._dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); - this._pos = 0; - } - _bad(what) { - throw new WeaveFFIError(-3, 'malformed value buffer: ' + what); - } - _take(n, what) { - if (this._pos + n > this._u8.length) this._bad('truncated ' + what); - const at = this._pos; - this._pos += n; - return at; - } - bool() { - const b = this._u8[this._take(1, 'bool')]; - if (b > 1) this._bad('bool byte out of range'); - return b === 1; - } - i8() { return this._dv.getInt8(this._take(1, 'i8')); } - u8() { return this._u8[this._take(1, 'u8')]; } - i16() { return this._dv.getInt16(this._take(2, 'i16'), true); } - u16() { return this._dv.getUint16(this._take(2, 'u16'), true); } - i32() { return this._dv.getInt32(this._take(4, 'i32'), true); } - u32() { return this._dv.getUint32(this._take(4, 'u32'), true); } - i64() { return this._dv.getBigInt64(this._take(8, 'i64'), true); } - u64() { return this._dv.getBigUint64(this._take(8, 'u64'), true); } - f32() { return this._dv.getFloat32(this._take(4, 'f32'), true); } - f64() { return this._dv.getFloat64(this._take(8, 'f64'), true); } - len() { - const n = this.u32(); - if (n > this._u8.length - this._pos) this._bad('length prefix exceeds remaining buffer'); - return n; - } - flag() { - const b = this._u8[this._take(1, 'option flag')]; - if (b > 1) this._bad('option flag byte out of range'); - return b === 1; - } - str() { - const n = this.len(); - const at = this._take(n, 'string bytes'); - try { - return _bufDec.decode(this._u8.subarray(at, at + n)); - } catch (e) { - this._bad('string is not valid UTF-8'); - } - } - bytes() { - const n = this.len(); - const at = this._take(n, 'byte buffer'); - return this._u8.subarray(at, at + n).slice(); - } - end() { - if (this._pos !== this._u8.length) this._bad('trailing bytes after value'); - } -} - -// Allocate a zeroed 16-byte error slot: -// { i32 code, char* message, uint8_t* payload_ptr, size_t payload_len }. -function _allocErr(wasm) { - const ptr = wasm.weaveffi_alloc(16); - new Uint8Array(wasm.memory.buffer, ptr, 16).fill(0); - return ptr; -} - -// Throw (and free the slot) if the error slot carries a non-zero code. -// Non-throwing wrappers route here: a non-zero code can only be a -// producer panic or a marshalling failure, surfaced as the generic -// WeaveFFIError. -function _checkErr(wasm, errPtr) { - const dv = new DataView(wasm.memory.buffer); - const code = dv.getInt32(errPtr, true); - if (code !== 0) { - const msgPtr = dv.getUint32(errPtr + 4, true); - const msg = _readCStr(wasm, msgPtr) || ''; - wasm.weaveffi_error_clear(errPtr); - wasm.weaveffi_dealloc(errPtr, 16); - throw new WeaveFFIError(code, msg); - } -} - -// Release an error slot on the success path. -function _freeErr(wasm, errPtr) { - wasm.weaveffi_dealloc(errPtr, 16); -} - -// Throw the `TaskError` domain error (and free the slot) if the error slot -// carries a non-zero code. -function _checkTaskError(wasm, errPtr) { - const dv = new DataView(wasm.memory.buffer); - const code = dv.getInt32(errPtr, true); - if (code !== 0) { - const msg = _readCStr(wasm, dv.getUint32(errPtr + 4, true)) || ''; - const _e = _taskErrorFrom(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)); - wasm.weaveffi_error_clear(errPtr); - wasm.weaveffi_dealloc(errPtr, 16); - throw _e; - } -} - -// Throw if a heap-boxed (consumer-owned) error carries a non-zero -// code. Used by async callbacks: the fields are read, then the box -// is released with weaveffi_error_free before throwing. -// `mkErr` maps domain codes (and decodes payload fields) for -// throwing callables; without it the generic WeaveFFIError is thrown. -function _checkErrRef(wasm, errPtr, mkErr) { - const dv = new DataView(wasm.memory.buffer); - const code = dv.getInt32(errPtr, true); - if (code === 0) { wasm.weaveffi_error_free(errPtr); return; } - const msg = _readCStr(wasm, dv.getUint32(errPtr + 4, true)) || ''; - const err = mkErr ? mkErr(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)) : new WeaveFFIError(code, msg); - wasm.weaveffi_error_free(errPtr); - throw err; -} - -// Serialize a `tasks.TaskResult` record into the value-buffer wire format. -function _write_tasks_TaskResult(w, v) { - w.i64(v.id); - w.str(v.value); - w.bool(v.success); -} - -// Decode a `tasks.TaskResult` record from the value-buffer wire format. -function _read_tasks_TaskResult(r) { - const v = {}; - v.id = r.i64(); - v.value = r.str(); - v.success = r.bool(); - return v; -} - -function _registerTrampoline(table, paramTypes, handler) { - const idx = table.grow(1); - table.set(idx, new WebAssembly.Function( - { parameters: paramTypes, results: [] }, - handler - )); - return idx; -} - -/** - * Load a WeaveFFI Wasm module from the given URL. - * - * @param {string} url - URL to the `.wasm` file. - * @returns {Promise} The API bindings. - * - * Exported functions follow the C ABI naming convention: - * weaveffi_{module}_{function}(params...) -> result - * - * @example - * const api = await loadWeaveffiWasm('lib.wasm'); - * - * // Primitive: plain numbers in, number out. - * const sum = api.math.add(1, 2); - * - * // Record: plain objects in and out (serialized automatically). - * const person = api.contacts.create({ name: 'Ada', age: 36 }); - * - * // Enum: pass the integer discriminant. - * api.ui.set_color(0); // 0 = first variant - * - * // Optional: pass null to omit, a value to provide. - * api.config.set_timeout(5000); // present - * api.config.set_timeout(null); // absent - * - * // List/Map: pass arrays/objects; receive arrays/objects. - * const names = api.data.all_names(); - */ -export async function loadWeaveffiWasm(url) { - const response = await fetch(url); - const bytes = await response.arrayBuffer(); - const { instance } = await WebAssembly.instantiate(bytes, {}); - const wasm = instance.exports; - - const _table = wasm.__indirect_function_table; - - let _nextCtxId = 1; - const _asyncContexts = new Map(); - - function _asyncHandler(ctxId, errPtr, ...results) { - const ctx = _asyncContexts.get(ctxId); - if (!ctx) return; - _asyncContexts.delete(ctxId); - try { - if (errPtr !== 0) _checkErrRef(wasm, errPtr, ctx.mkErr); - ctx.resolve(ctx.unwrap ? ctx.unwrap(wasm, ...results) : results[0]); - } catch (e) { - ctx.reject(e); - } - } - - const _cbPtr_i32_i32_i32_i32 = _registerTrampoline(_table, ['i32', 'i32', 'i32', 'i32'], _asyncHandler); - const _cbPtr_i32_i32_i32 = _registerTrampoline(_table, ['i32', 'i32', 'i32'], _asyncHandler); - - return { - _raw: wasm, - tasks: { - runTask(name) { - return new Promise((resolve, reject) => { - const ctxId = _nextCtxId++; - _asyncContexts.set(ctxId, { resolve, reject, mkErr: _taskErrorFrom, unwrap: (w, ptr, len) => { - const _b = ptr === 0 || len === 0 ? new Uint8Array(0) : new Uint8Array(w.memory.buffer, ptr, len).slice(); - if (ptr !== 0) w.weaveffi_free_bytes(ptr, len); - const _rd = new _BufReader(_b); - const _v = _read_tasks_TaskResult(_rd); - _rd.end(); - return _v; - } }); - const [a0_p, a0_s] = _cstr(wasm, name); - wasm.weaveffi_tasks_run_task_async(a0_p, _cbPtr_i32_i32_i32_i32, ctxId); - wasm.weaveffi_dealloc(a0_p, a0_s); - }); - }, - runBatch(names) { - return new Promise((resolve, reject) => { - const ctxId = _nextCtxId++; - _asyncContexts.set(ctxId, { resolve, reject, unwrap: (w, ptr, len) => { - const _b = ptr === 0 || len === 0 ? new Uint8Array(0) : new Uint8Array(w.memory.buffer, ptr, len).slice(); - if (ptr !== 0) w.weaveffi_free_bytes(ptr, len); - const _rd = new _BufReader(_b); - const _v = (() => { const _n = _rd.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push(_read_tasks_TaskResult(_rd)); return _arr; })(); - _rd.end(); - return _v; - } }); - const a0_w = new _BufWriter(); - const _a1 = names || []; - a0_w.len(_a1.length); - for (const _e1 of _a1) { - a0_w.str(_e1); - } - const [a0_p, a0_l] = _bytes(wasm, a0_w.finish()); - wasm.weaveffi_tasks_run_batch_async(a0_p, a0_l, _cbPtr_i32_i32_i32_i32, ctxId); - wasm.weaveffi_dealloc(a0_p, a0_l); - }); - }, - cancelTask(id) { - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_tasks_cancel_task(BigInt(id), _err); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r !== 0; - }, - runNTasks(n) { - return new Promise((resolve, reject) => { - const ctxId = _nextCtxId++; - _asyncContexts.set(ctxId, { resolve, reject }); - wasm.weaveffi_tasks_run_n_tasks_async(n, _cbPtr_i32_i32_i32, ctxId); - }); - }, - activeCallbacks() { - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_tasks_active_callbacks(_err); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r; - }, - }, - }; -} - -// END weaveffi_wasm.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_calculator__README_md.snap b/crates/weaveffi-cli/tests/snapshots/wasm_calculator__README_md.snap deleted file mode 100644 index f41cc6cc..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_calculator__README_md.snap +++ /dev/null @@ -1,126 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI Wasm (experimental) - -This folder contains a minimal stub to help you load a `wasm32-unknown-unknown` build of your WeaveFFI library. - -Build (example): - -```bash -cargo build --target wasm32-unknown-unknown --release -``` - -Then serve the `.wasm` and use `weaveffi_wasm.js` to load it. - -## Complex Type Handling - -Wasm only supports numeric types natively (`i32`, `i64`, `f32`, `f64`). Complex types are encoded at the boundary as follows: - -### Records - -Records are **plain JavaScript objects**. A record crosses the boundary serialized in the WeaveFFI value-buffer format as one pointer + length pair in linear memory; the glue packs and unpacks it automatically, so there are no handles, no accessor functions, and nothing to free. - -### Enums - -C-style enums are passed as **`i32` values** corresponding to the variant's integer discriminant. Rich (algebraic) enums are plain objects tagged by variant name (`{ tag: "Circle", radius: 2 }`) and cross the boundary serialized in a value buffer, exactly like records. - -### Optionals - -Optional values map to **`null`** for the absent case. An optional crosses the boundary inside a value buffer as a one-byte presence flag followed by the value when present. The one exception is an optional interface, which stays a nullable object pointer (`0` = absent). - -### Lists and Maps - -Lists are JS arrays; maps are plain objects (a `Map` instance is also accepted on input). Both cross the boundary serialized in a value buffer as one pointer + length pair, recursing through element types. - -### Iterators - -`iter` functions return a **lazy JS iterator** (typed `IterableIterator`): each `next()` issues exactly one producer call, so iteration streams in constant memory. The producer handle is destroyed exactly once, on exhaustion or via `return()` when iteration stops early (a `for...of` loop calls `return()` automatically on `break` or `throw`). Abandoning an iterator without exhausting or closing it leaks the handle. - -### Error Handling - -The generated JS wrappers automatically handle errors by passing an error -pointer as the last argument to each Wasm function. The error struct is -16 bytes on wasm32: `{ i32 code, char* message, uint8_t* payload_ptr, -size_t payload_len }`. Your Wasm module must export the following -functions: - -- `weaveffi_alloc(size: i32) -> i32`: allocate `size` bytes in linear memory -- `weaveffi_dealloc(ptr: i32, size: i32)`: release a `weaveffi_alloc` block -- `weaveffi_error_clear(err_ptr: i32)`: clear and free error resources -- `weaveffi_free_string(ptr: i32)`: free a producer-returned C string -- `weaveffi_free_bytes(ptr: i32, len: i32)`: free a producer-returned buffer - -Wrappers of functions declared `throws` raise the declaring module's typed -error class (a `WeaveFFIError` subclass with a per-code subclass, such as -`KeyNotFound`); every other wrapper raises the generic `WeaveFFIError` only -for producer panics and marshalling failures. When the matched error code -declares payload fields, the wrapper decodes them from the error's value -buffer and attaches them as properties on the thrown error. - -## API Reference - -### Module: `calculator` - -#### Error Domain: `CalcError` - -Throwing wrappers in this module raise `CalcError` (a `WeaveFFIError` subclass); each code below is its own subclass carrying the stable `code`. Payload fields are decoded from the error's value buffer and attached as properties on the thrown error. - -| Class | Code | Default Message | Payload Fields | -|-------|------|-----------------|----------------| -| `DivisionByZero` | `1` | division by zero | (none) | - -#### Functions - -##### `weaveffi_calculator_add` - -Add two integers - -`weaveffi_calculator_add(a: i32, b: i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `a` | `i32` | `i32` | native Wasm i32 | -| `b` | `i32` | `i32` | native Wasm i32 | -| _returns_ | `i32` | `i32` | native Wasm i32 | - -##### `weaveffi_calculator_mul` - -Multiply two integers - -`weaveffi_calculator_mul(a: i32, b: i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `a` | `i32` | `i32` | native Wasm i32 | -| `b` | `i32` | `i32` | native Wasm i32 | -| _returns_ | `i32` | `i32` | native Wasm i32 | - -##### `weaveffi_calculator_div` - -Divide two integers, failing on a zero divisor - -`weaveffi_calculator_div(a: i32, b: i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `a` | `i32` | `i32` | native Wasm i32 | -| `b` | `i32` | `i32` | native Wasm i32 | -| _returns_ | `i32` | `i32` | native Wasm i32 | - -##### `weaveffi_calculator_echo` - -Echo a string back - -`weaveffi_calculator_echo(s: i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `s` | `string` | `i32` | NUL-terminated C string pointer | -| _returns_ | `string` | `i32` | NUL-terminated C string pointer | - - diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_calculator__package_json.snap b/crates/weaveffi-cli/tests/snapshots/wasm_calculator__package_json.snap deleted file mode 100644 index cee4ead6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_calculator__package_json.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -{ - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", - "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", - "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", - "type": "module", - "main": "weaveffi_wasm.js", - "types": "weaveffi_wasm.d.ts" -} diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_calculator__weaveffi_wasm_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_calculator__weaveffi_wasm_d_ts.snap deleted file mode 100644 index 3f6c058f..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_calculator__weaveffi_wasm_d_ts.snap +++ /dev/null @@ -1,57 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// Generated TypeScript declarations for WeaveFFI Wasm bindings - -/** Base error for WeaveFFI failures: domain errors extend it, and it is - * thrown directly for unknown codes, marshalling failures, and producer - * panics. Carries the stable ABI `code`. */ -export declare class WeaveFFIError extends Error { - constructor(code: number, message?: string); - code: number; -} - -/** Base error for the `calculator` module's error domain. */ -export declare class CalcError extends WeaveFFIError { - static readonly DivisionByZero: typeof DivisionByZero; -} - -/** division by zero */ -export declare class DivisionByZero extends CalcError { - constructor(message?: string); - static readonly CODE: 1; -} - -export interface WeaveffiWasmModule { - _raw: WebAssembly.Exports; - calculator: { - /** - * Add two integers - * @throws {WeaveFFIError} if the native call fails - */ - add(a: number, b: number): number; - /** - * Multiply two integers - * @throws {WeaveFFIError} if the native call fails - */ - mul(a: number, b: number): number; - /** - * Divide two integers, failing on a zero divisor - * @throws {CalcError} on a domain error code - */ - div(a: number, b: number): number; - /** - * Echo a string back - * @throws {WeaveFFIError} if the native call fails - */ - echo(s: string): string; - }; -} - -export function loadWeaveffiWasm(url: string): Promise; - -// END weaveffi_wasm.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_calculator__weaveffi_wasm_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_calculator__weaveffi_wasm_js.snap deleted file mode 100644 index e39b02da..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_calculator__weaveffi_wasm_js.snap +++ /dev/null @@ -1,200 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// WeaveFFI Wasm bindings (auto-generated) -// -// Boundary conventions for a wasm32-unknown-unknown build: -// -// Objects -> i32 pointer into linear memory (0 = null/absent) -// Enums -> i32 discriminant value -// i64/u64 -> JavaScript BigInt -// Strings -> NUL-terminated UTF-8 (const char*); a single i32 pointer -// Bytes -> i32 data pointer + i32 length (out_len for returns) -// Buffered -> records, rich enums, optionals, lists, and maps cross -// as one value buffer: i32 pointer + i32 length - -/** Base error for WeaveFFI failures: domain errors extend it, and it is - * thrown directly for unknown codes, marshalling failures, and producer - * panics. Carries the stable ABI `code`. */ -export class WeaveFFIError extends Error { - constructor(code, message) { - super(message ? `WeaveFFI error ${code}: ${message}` : `WeaveFFI error ${code}`); - this.name = new.target.name; - this.code = code; - } -} - -/** Base error for the `calculator` module's error domain. */ -export class CalcError extends WeaveFFIError {} - -// division by zero -export class DivisionByZero extends CalcError { - constructor(message = "division by zero") { - super(1, message); - } -} -DivisionByZero.CODE = 1; -CalcError.DivisionByZero = DivisionByZero; - -const _CALC_ERROR_CODES = Object.freeze({ - 1: DivisionByZero, -}); - -// Build the CalcError subclass matching `code`, or a generic -// WeaveFFIError for codes outside the domain (panics, marshalling). -function _calcErrorFrom(wasm, code, message, payloadPtr, payloadLen) { - const _cls = _CALC_ERROR_CODES[code]; - const _e = _cls ? (message ? new _cls(message) : new _cls()) : new WeaveFFIError(code, message); - return _e; -} - -const _enc = new TextEncoder(); -const _dec = new TextDecoder(); - -// Stage a JS string as a NUL-terminated C string in linear memory. -// Returns [ptr, size] (size includes the NUL); release with _free. -function _cstr(wasm, str) { - const bytes = _enc.encode(str); - const size = bytes.length + 1; - const ptr = wasm.weaveffi_alloc(size); - const mem = new Uint8Array(wasm.memory.buffer, ptr, size); - mem.set(bytes); - mem[bytes.length] = 0; - return [ptr, size]; -} - -// Read a NUL-terminated C string (0 => null). Does not free. -function _readCStr(wasm, ptr) { - if (ptr === 0) return null; - const mem = new Uint8Array(wasm.memory.buffer); - let end = ptr; - while (mem[end] !== 0) end++; - return _dec.decode(mem.subarray(ptr, end)); -} - -// Read then free a producer-owned C string. -function _takeCStr(wasm, ptr) { - const s = _readCStr(wasm, ptr); - if (ptr !== 0) wasm.weaveffi_free_string(ptr); - return s; -} - -// Allocate a zeroed 16-byte error slot: -// { i32 code, char* message, uint8_t* payload_ptr, size_t payload_len }. -function _allocErr(wasm) { - const ptr = wasm.weaveffi_alloc(16); - new Uint8Array(wasm.memory.buffer, ptr, 16).fill(0); - return ptr; -} - -// Throw (and free the slot) if the error slot carries a non-zero code. -// Non-throwing wrappers route here: a non-zero code can only be a -// producer panic or a marshalling failure, surfaced as the generic -// WeaveFFIError. -function _checkErr(wasm, errPtr) { - const dv = new DataView(wasm.memory.buffer); - const code = dv.getInt32(errPtr, true); - if (code !== 0) { - const msgPtr = dv.getUint32(errPtr + 4, true); - const msg = _readCStr(wasm, msgPtr) || ''; - wasm.weaveffi_error_clear(errPtr); - wasm.weaveffi_dealloc(errPtr, 16); - throw new WeaveFFIError(code, msg); - } -} - -// Release an error slot on the success path. -function _freeErr(wasm, errPtr) { - wasm.weaveffi_dealloc(errPtr, 16); -} - -// Throw the `CalcError` domain error (and free the slot) if the error slot -// carries a non-zero code. -function _checkCalcError(wasm, errPtr) { - const dv = new DataView(wasm.memory.buffer); - const code = dv.getInt32(errPtr, true); - if (code !== 0) { - const msg = _readCStr(wasm, dv.getUint32(errPtr + 4, true)) || ''; - const _e = _calcErrorFrom(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)); - wasm.weaveffi_error_clear(errPtr); - wasm.weaveffi_dealloc(errPtr, 16); - throw _e; - } -} - -/** - * Load a WeaveFFI Wasm module from the given URL. - * - * @param {string} url - URL to the `.wasm` file. - * @returns {Promise} The API bindings. - * - * Exported functions follow the C ABI naming convention: - * weaveffi_{module}_{function}(params...) -> result - * - * @example - * const api = await loadWeaveffiWasm('lib.wasm'); - * - * // Primitive: plain numbers in, number out. - * const sum = api.math.add(1, 2); - * - * // Record: plain objects in and out (serialized automatically). - * const person = api.contacts.create({ name: 'Ada', age: 36 }); - * - * // Enum: pass the integer discriminant. - * api.ui.set_color(0); // 0 = first variant - * - * // Optional: pass null to omit, a value to provide. - * api.config.set_timeout(5000); // present - * api.config.set_timeout(null); // absent - * - * // List/Map: pass arrays/objects; receive arrays/objects. - * const names = api.data.all_names(); - */ -export async function loadWeaveffiWasm(url) { - const response = await fetch(url); - const bytes = await response.arrayBuffer(); - const { instance } = await WebAssembly.instantiate(bytes, {}); - const wasm = instance.exports; - - return { - _raw: wasm, - calculator: { - add(a, b) { - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_calculator_add(a, b, _err); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r; - }, - mul(a, b) { - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_calculator_mul(a, b, _err); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r; - }, - div(a, b) { - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_calculator_div(a, b, _err); - _checkCalcError(wasm, _err); - _freeErr(wasm, _err); - return _r; - }, - echo(s) { - const [a0_p, a0_s] = _cstr(wasm, s); - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_calculator_echo(a0_p, _err); - wasm.weaveffi_dealloc(a0_p, a0_s); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _takeCStr(wasm, _r); - }, - }, - }; -} - -// END weaveffi_wasm.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_contacts__README_md.snap b/crates/weaveffi-cli/tests/snapshots/wasm_contacts__README_md.snap deleted file mode 100644 index c2156872..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_contacts__README_md.snap +++ /dev/null @@ -1,170 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI Wasm (experimental) - -This folder contains a minimal stub to help you load a `wasm32-unknown-unknown` build of your WeaveFFI library. - -Build (example): - -```bash -cargo build --target wasm32-unknown-unknown --release -``` - -Then serve the `.wasm` and use `weaveffi_wasm.js` to load it. - -## Complex Type Handling - -Wasm only supports numeric types natively (`i32`, `i64`, `f32`, `f64`). Complex types are encoded at the boundary as follows: - -### Records - -Records are **plain JavaScript objects**. A record crosses the boundary serialized in the WeaveFFI value-buffer format as one pointer + length pair in linear memory; the glue packs and unpacks it automatically, so there are no handles, no accessor functions, and nothing to free. - -### Enums - -C-style enums are passed as **`i32` values** corresponding to the variant's integer discriminant. Rich (algebraic) enums are plain objects tagged by variant name (`{ tag: "Circle", radius: 2 }`) and cross the boundary serialized in a value buffer, exactly like records. - -### Optionals - -Optional values map to **`null`** for the absent case. An optional crosses the boundary inside a value buffer as a one-byte presence flag followed by the value when present. The one exception is an optional interface, which stays a nullable object pointer (`0` = absent). - -### Lists and Maps - -Lists are JS arrays; maps are plain objects (a `Map` instance is also accepted on input). Both cross the boundary serialized in a value buffer as one pointer + length pair, recursing through element types. - -### Iterators - -`iter` functions return a **lazy JS iterator** (typed `IterableIterator`): each `next()` issues exactly one producer call, so iteration streams in constant memory. The producer handle is destroyed exactly once, on exhaustion or via `return()` when iteration stops early (a `for...of` loop calls `return()` automatically on `break` or `throw`). Abandoning an iterator without exhausting or closing it leaks the handle. - -### Error Handling - -The generated JS wrappers automatically handle errors by passing an error -pointer as the last argument to each Wasm function. The error struct is -16 bytes on wasm32: `{ i32 code, char* message, uint8_t* payload_ptr, -size_t payload_len }`. Your Wasm module must export the following -functions: - -- `weaveffi_alloc(size: i32) -> i32`: allocate `size` bytes in linear memory -- `weaveffi_dealloc(ptr: i32, size: i32)`: release a `weaveffi_alloc` block -- `weaveffi_error_clear(err_ptr: i32)`: clear and free error resources -- `weaveffi_free_string(ptr: i32)`: free a producer-returned C string -- `weaveffi_free_bytes(ptr: i32, len: i32)`: free a producer-returned buffer - -Wrappers of functions declared `throws` raise the declaring module's typed -error class (a `WeaveFFIError` subclass with a per-code subclass, such as -`KeyNotFound`); every other wrapper raises the generic `WeaveFFIError` only -for producer panics and marshalling failures. When the matched error code -declares payload fields, the wrapper decodes them from the error's value -buffer and attaches them as properties on the thrown error. - -## API Reference - -### Module: `contacts` - -#### Error Domain: `ContactsError` - -Throwing wrappers in this module raise `ContactsError` (a `WeaveFFIError` subclass); each code below is its own subclass carrying the stable `code`. Payload fields are decoded from the error's value buffer and attached as properties on the thrown error. - -| Class | Code | Default Message | Payload Fields | -|-------|------|-----------------|----------------| -| `InvalidName` | `1` | name must not be empty | (none) | -| `NotFound` | `2` | contact not found | (none) | - -#### Interfaces - -##### `ContactBook` - -An in-memory address book owning its contacts - -Passed as an **opaque object pointer** (`i32`), wrapped by a JS class. Constructors return an owned handle; methods pass the handle as the implicit leading `self` argument; `free()` releases the handle via the destroy symbol. - -##### `weaveffi_contacts_ContactBook_new` - -`weaveffi_contacts_ContactBook_new() -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `ContactBook` | `i32` | opaque object pointer | - -##### `weaveffi_contacts_ContactBook_add` - -`weaveffi_contacts_ContactBook_add(first_name: i32, last_name: i32, email: i32, i32, contact_type: i32) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `first_name` | `string` | `i32` | NUL-terminated C string pointer | -| `last_name` | `string` | `i32` | NUL-terminated C string pointer | -| `email` | `string?` | `i32, i32` | value buffer: ptr + len in linear memory | -| `contact_type` | `ContactType` | `i32` | variant discriminant | -| _returns_ | `Contact` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_contacts_ContactBook_get` - -`weaveffi_contacts_ContactBook_get(id: i64) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `id` | `i64` | `i64` | native Wasm i64 | -| _returns_ | `Contact` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_contacts_ContactBook_list` - -`weaveffi_contacts_ContactBook_list() -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `[Contact]` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_contacts_ContactBook_remove` - -`weaveffi_contacts_ContactBook_remove(id: i64) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `id` | `i64` | `i64` | native Wasm i64 | -| _returns_ | `bool` | `i32` | 0 = false, 1 = true | - -##### `weaveffi_contacts_ContactBook_count` - -`weaveffi_contacts_ContactBook_count() -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `i32` | `i32` | native Wasm i32 | - -##### `weaveffi_contacts_ContactBook_destroy` - -Releases the object reference. Called by the wrapper's `free()`. - -#### Structs - -##### `Contact` - -A plain JS object, serialized in a value buffer (`i32` ptr + `i32` len) at the boundary. Fields in declaration (wire) order: - -| Field | Type | -|-------|------| -| `id` | `i64` | -| `first_name` | `string` | -| `last_name` | `string` | -| `email` | `string?` | -| `contact_type` | `ContactType` | - -#### Enums - -##### `ContactType` - -Passed as `i32` discriminant. - -| Variant | Value | -|---------|-------| -| `Personal` | `0` | -| `Work` | `1` | -| `Other` | `2` | - - diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_contacts__package_json.snap b/crates/weaveffi-cli/tests/snapshots/wasm_contacts__package_json.snap deleted file mode 100644 index cee4ead6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_contacts__package_json.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -{ - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", - "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", - "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", - "type": "module", - "main": "weaveffi_wasm.js", - "types": "weaveffi_wasm.d.ts" -} diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_contacts__weaveffi_wasm_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_contacts__weaveffi_wasm_d_ts.snap deleted file mode 100644 index 58ad0496..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_contacts__weaveffi_wasm_d_ts.snap +++ /dev/null @@ -1,89 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// Generated TypeScript declarations for WeaveFFI Wasm bindings - -/** Base error for WeaveFFI failures: domain errors extend it, and it is - * thrown directly for unknown codes, marshalling failures, and producer - * panics. Carries the stable ABI `code`. */ -export declare class WeaveFFIError extends Error { - constructor(code: number, message?: string); - code: number; -} - -/** Base error for the `contacts` module's error domain. */ -export declare class ContactsError extends WeaveFFIError { - static readonly InvalidName: typeof InvalidName; - static readonly NotFound: typeof NotFound; -} - -/** name must not be empty */ -export declare class InvalidName extends ContactsError { - constructor(message?: string); - static readonly CODE: 1; -} - -/** contact not found */ -export declare class NotFound extends ContactsError { - constructor(message?: string); - static readonly CODE: 2; -} - -export interface Contact { - id: bigint; - first_name: string; - last_name: string; - email: string | null; - contact_type: ContactType; -} - -export declare const ContactType: Readonly<{ - Personal: 0; - Work: 1; - Other: 2; -}>; - -/** An in-memory address book owning its contacts */ -export declare class ContactBook { - /** - * @throws {WeaveFFIError} if the native call fails - */ - constructor(); - /** - * @throws {ContactsError} on a domain error code - */ - add(firstName: string, lastName: string, email: string | null, contactType: ContactType): Contact; - /** - * @throws {ContactsError} on a domain error code - */ - get(id: bigint): Contact; - /** - * @throws {WeaveFFIError} if the native call fails - */ - list(): Contact[]; - /** - * @throws {WeaveFFIError} if the native call fails - */ - remove(id: bigint): boolean; - /** - * @throws {WeaveFFIError} if the native call fails - */ - count(): number; - /** Releases the producer-owned handle exactly once. */ - free(): void; -} - -export interface WeaveffiWasmModule { - _raw: WebAssembly.Exports; - contacts: { - ContactBook: typeof ContactBook; - }; -} - -export function loadWeaveffiWasm(url: string): Promise; - -// END weaveffi_wasm.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_contacts__weaveffi_wasm_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_contacts__weaveffi_wasm_js.snap deleted file mode 100644 index 90dc7570..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_contacts__weaveffi_wasm_js.snap +++ /dev/null @@ -1,431 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// WeaveFFI Wasm bindings (auto-generated) -// -// Boundary conventions for a wasm32-unknown-unknown build: -// -// Objects -> i32 pointer into linear memory (0 = null/absent) -// Enums -> i32 discriminant value -// i64/u64 -> JavaScript BigInt -// Strings -> NUL-terminated UTF-8 (const char*); a single i32 pointer -// Bytes -> i32 data pointer + i32 length (out_len for returns) -// Buffered -> records, rich enums, optionals, lists, and maps cross -// as one value buffer: i32 pointer + i32 length - -/** Base error for WeaveFFI failures: domain errors extend it, and it is - * thrown directly for unknown codes, marshalling failures, and producer - * panics. Carries the stable ABI `code`. */ -export class WeaveFFIError extends Error { - constructor(code, message) { - super(message ? `WeaveFFI error ${code}: ${message}` : `WeaveFFI error ${code}`); - this.name = new.target.name; - this.code = code; - } -} - -/** Base error for the `contacts` module's error domain. */ -export class ContactsError extends WeaveFFIError {} - -// name must not be empty -export class InvalidName extends ContactsError { - constructor(message = "name must not be empty") { - super(1, message); - } -} -InvalidName.CODE = 1; -ContactsError.InvalidName = InvalidName; - -// contact not found -export class NotFound extends ContactsError { - constructor(message = "contact not found") { - super(2, message); - } -} -NotFound.CODE = 2; -ContactsError.NotFound = NotFound; - -const _CONTACTS_ERROR_CODES = Object.freeze({ - 1: InvalidName, - 2: NotFound, -}); - -// Build the ContactsError subclass matching `code`, or a generic -// WeaveFFIError for codes outside the domain (panics, marshalling). -function _contactsErrorFrom(wasm, code, message, payloadPtr, payloadLen) { - const _cls = _CONTACTS_ERROR_CODES[code]; - const _e = _cls ? (message ? new _cls(message) : new _cls()) : new WeaveFFIError(code, message); - return _e; -} - -const _enc = new TextEncoder(); -const _dec = new TextDecoder(); - -// Stage a JS string as a NUL-terminated C string in linear memory. -// Returns [ptr, size] (size includes the NUL); release with _free. -function _cstr(wasm, str) { - const bytes = _enc.encode(str); - const size = bytes.length + 1; - const ptr = wasm.weaveffi_alloc(size); - const mem = new Uint8Array(wasm.memory.buffer, ptr, size); - mem.set(bytes); - mem[bytes.length] = 0; - return [ptr, size]; -} - -// Read a NUL-terminated C string (0 => null). Does not free. -function _readCStr(wasm, ptr) { - if (ptr === 0) return null; - const mem = new Uint8Array(wasm.memory.buffer); - let end = ptr; - while (mem[end] !== 0) end++; - return _dec.decode(mem.subarray(ptr, end)); -} - -// Read then free a producer-owned C string. -function _takeCStr(wasm, ptr) { - const s = _readCStr(wasm, ptr); - if (ptr !== 0) wasm.weaveffi_free_string(ptr); - return s; -} - -// Stage a byte buffer (or an encoded value buffer); returns [ptr, len]; -// release with weaveffi_dealloc(ptr, len). -function _bytes(wasm, data) { - const u8 = data instanceof Uint8Array ? data : new Uint8Array(data); - const ptr = wasm.weaveffi_alloc(u8.length); - if (u8.length) new Uint8Array(wasm.memory.buffer, ptr, u8.length).set(u8); - return [ptr, u8.length]; -} - -// Copy then free a producer-owned byte (or value) buffer. -function _takeBytes(wasm, ptr, len) { - if (ptr === 0 || len === 0) return new Uint8Array(0); - const copy = new Uint8Array(wasm.memory.buffer, ptr, len).slice(); - wasm.weaveffi_free_bytes(ptr, len); - return copy; -} - -// Growable little-endian byte writer implementing the WeaveFFI value-buffer -// wire format: values are packed back to back with no alignment; lengths and -// counts are u32; strings are u32 byte length + UTF-8 bytes (no NUL). -class _BufWriter { - constructor() { - this._u8 = new Uint8Array(64); - this._dv = new DataView(this._u8.buffer); - this._len = 0; - } - _need(n) { - if (this._len + n <= this._u8.length) return; - let cap = this._u8.length * 2; - while (cap < this._len + n) cap *= 2; - const u8 = new Uint8Array(cap); - u8.set(this._u8.subarray(0, this._len)); - this._u8 = u8; - this._dv = new DataView(u8.buffer); - } - bool(v) { this._need(1); this._dv.setUint8(this._len, v ? 1 : 0); this._len += 1; } - i8(v) { this._need(1); this._dv.setInt8(this._len, v); this._len += 1; } - u8(v) { this._need(1); this._dv.setUint8(this._len, v); this._len += 1; } - i16(v) { this._need(2); this._dv.setInt16(this._len, v, true); this._len += 2; } - u16(v) { this._need(2); this._dv.setUint16(this._len, v, true); this._len += 2; } - i32(v) { this._need(4); this._dv.setInt32(this._len, v, true); this._len += 4; } - u32(v) { this._need(4); this._dv.setUint32(this._len, v, true); this._len += 4; } - i64(v) { this._need(8); this._dv.setBigInt64(this._len, BigInt(v), true); this._len += 8; } - u64(v) { this._need(8); this._dv.setBigUint64(this._len, BigInt(v), true); this._len += 8; } - f32(v) { this._need(4); this._dv.setFloat32(this._len, v, true); this._len += 4; } - f64(v) { this._need(8); this._dv.setFloat64(this._len, v, true); this._len += 8; } - len(n) { this.u32(n); } - flag(present) { this.u8(present ? 1 : 0); } - str(v) { - const b = _enc.encode(v); - this.len(b.length); - this._need(b.length); - this._u8.set(b, this._len); - this._len += b.length; - } - bytes(v) { - const b = v instanceof Uint8Array ? v : new Uint8Array(v); - this.len(b.length); - this._need(b.length); - this._u8.set(b, this._len); - this._len += b.length; - } - finish() { return this._u8.subarray(0, this._len); } -} - -const _bufDec = new TextDecoder('utf-8', { fatal: true }); - -// Strict little-endian reader for the WeaveFFI value-buffer wire format. A -// malformed buffer (truncation, invalid bool or flag bytes, an oversized -// length prefix, invalid UTF-8, trailing bytes) is a producer/consumer -// contract violation and throws the generic brand error; code -3 marks a -// consumer-side marshalling failure. -class _BufReader { - constructor(bytes) { - this._u8 = bytes; - this._dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); - this._pos = 0; - } - _bad(what) { - throw new WeaveFFIError(-3, 'malformed value buffer: ' + what); - } - _take(n, what) { - if (this._pos + n > this._u8.length) this._bad('truncated ' + what); - const at = this._pos; - this._pos += n; - return at; - } - bool() { - const b = this._u8[this._take(1, 'bool')]; - if (b > 1) this._bad('bool byte out of range'); - return b === 1; - } - i8() { return this._dv.getInt8(this._take(1, 'i8')); } - u8() { return this._u8[this._take(1, 'u8')]; } - i16() { return this._dv.getInt16(this._take(2, 'i16'), true); } - u16() { return this._dv.getUint16(this._take(2, 'u16'), true); } - i32() { return this._dv.getInt32(this._take(4, 'i32'), true); } - u32() { return this._dv.getUint32(this._take(4, 'u32'), true); } - i64() { return this._dv.getBigInt64(this._take(8, 'i64'), true); } - u64() { return this._dv.getBigUint64(this._take(8, 'u64'), true); } - f32() { return this._dv.getFloat32(this._take(4, 'f32'), true); } - f64() { return this._dv.getFloat64(this._take(8, 'f64'), true); } - len() { - const n = this.u32(); - if (n > this._u8.length - this._pos) this._bad('length prefix exceeds remaining buffer'); - return n; - } - flag() { - const b = this._u8[this._take(1, 'option flag')]; - if (b > 1) this._bad('option flag byte out of range'); - return b === 1; - } - str() { - const n = this.len(); - const at = this._take(n, 'string bytes'); - try { - return _bufDec.decode(this._u8.subarray(at, at + n)); - } catch (e) { - this._bad('string is not valid UTF-8'); - } - } - bytes() { - const n = this.len(); - const at = this._take(n, 'byte buffer'); - return this._u8.subarray(at, at + n).slice(); - } - end() { - if (this._pos !== this._u8.length) this._bad('trailing bytes after value'); - } -} - -// Allocate a zeroed 16-byte error slot: -// { i32 code, char* message, uint8_t* payload_ptr, size_t payload_len }. -function _allocErr(wasm) { - const ptr = wasm.weaveffi_alloc(16); - new Uint8Array(wasm.memory.buffer, ptr, 16).fill(0); - return ptr; -} - -// Throw (and free the slot) if the error slot carries a non-zero code. -// Non-throwing wrappers route here: a non-zero code can only be a -// producer panic or a marshalling failure, surfaced as the generic -// WeaveFFIError. -function _checkErr(wasm, errPtr) { - const dv = new DataView(wasm.memory.buffer); - const code = dv.getInt32(errPtr, true); - if (code !== 0) { - const msgPtr = dv.getUint32(errPtr + 4, true); - const msg = _readCStr(wasm, msgPtr) || ''; - wasm.weaveffi_error_clear(errPtr); - wasm.weaveffi_dealloc(errPtr, 16); - throw new WeaveFFIError(code, msg); - } -} - -// Release an error slot on the success path. -function _freeErr(wasm, errPtr) { - wasm.weaveffi_dealloc(errPtr, 16); -} - -// Throw the `ContactsError` domain error (and free the slot) if the error slot -// carries a non-zero code. -function _checkContactsError(wasm, errPtr) { - const dv = new DataView(wasm.memory.buffer); - const code = dv.getInt32(errPtr, true); - if (code !== 0) { - const msg = _readCStr(wasm, dv.getUint32(errPtr + 4, true)) || ''; - const _e = _contactsErrorFrom(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)); - wasm.weaveffi_error_clear(errPtr); - wasm.weaveffi_dealloc(errPtr, 16); - throw _e; - } -} - -// Serialize a `contacts.Contact` record into the value-buffer wire format. -function _write_contacts_Contact(w, v) { - w.i64(v.id); - w.str(v.first_name); - w.str(v.last_name); - if (v.email === null || v.email === undefined) { - w.flag(false); - } else { - w.flag(true); - w.str(v.email); - } - w.i32(v.contact_type); -} - -// Decode a `contacts.Contact` record from the value-buffer wire format. -function _read_contacts_Contact(r) { - const v = {}; - v.id = r.i64(); - v.first_name = r.str(); - v.last_name = r.str(); - v.email = (r.flag() ? r.str() : null); - v.contact_type = r.i32(); - return v; -} - -export const ContactType = Object.freeze({ - Personal: 0, - Work: 1, - Other: 2, -}); - -/** - * Load a WeaveFFI Wasm module from the given URL. - * - * @param {string} url - URL to the `.wasm` file. - * @returns {Promise} The API bindings. - * - * Exported functions follow the C ABI naming convention: - * weaveffi_{module}_{function}(params...) -> result - * - * @example - * const api = await loadWeaveffiWasm('lib.wasm'); - * - * // Primitive: plain numbers in, number out. - * const sum = api.math.add(1, 2); - * - * // Record: plain objects in and out (serialized automatically). - * const person = api.contacts.create({ name: 'Ada', age: 36 }); - * - * // Enum: pass the integer discriminant. - * api.ui.set_color(0); // 0 = first variant - * - * // Optional: pass null to omit, a value to provide. - * api.config.set_timeout(5000); // present - * api.config.set_timeout(null); // absent - * - * // List/Map: pass arrays/objects; receive arrays/objects. - * const names = api.data.all_names(); - */ -export async function loadWeaveffiWasm(url) { - const response = await fetch(url); - const bytes = await response.arrayBuffer(); - const { instance } = await WebAssembly.instantiate(bytes, {}); - const wasm = instance.exports; - - // An in-memory address book owning its contacts - class ContactBook { - constructor() { - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_contacts_ContactBook_new(_err); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - this._handle = _r; - } - static _wrap(handle) { - const _o = Object.create(ContactBook.prototype); - _o._handle = handle; - return _o; - } - free() { - if (this._handle !== 0) { - wasm.weaveffi_contacts_ContactBook_destroy(this._handle); - this._handle = 0; - } - } - add(firstName, lastName, email, contactType) { - const [a0_p, a0_s] = _cstr(wasm, firstName); - const [a1_p, a1_s] = _cstr(wasm, lastName); - const a2_w = new _BufWriter(); - if (email === null || email === undefined) { - a2_w.flag(false); - } else { - a2_w.flag(true); - a2_w.str(email); - } - const [a2_p, a2_l] = _bytes(wasm, a2_w.finish()); - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_contacts_ContactBook_add(this._handle, a0_p, a1_p, a2_p, a2_l, contactType, _lp, _err); - wasm.weaveffi_dealloc(a0_p, a0_s); - wasm.weaveffi_dealloc(a1_p, a1_s); - wasm.weaveffi_dealloc(a2_p, a2_l); - _checkContactsError(wasm, _err); - _freeErr(wasm, _err); - const _len = new DataView(wasm.memory.buffer).getUint32(_lp, true); - wasm.weaveffi_dealloc(_lp, 4); - const _rd = new _BufReader(_takeBytes(wasm, _r, _len)); - const _out = _read_contacts_Contact(_rd); - _rd.end(); - return _out; - } - get(id) { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_contacts_ContactBook_get(this._handle, BigInt(id), _lp, _err); - _checkContactsError(wasm, _err); - _freeErr(wasm, _err); - const _len = new DataView(wasm.memory.buffer).getUint32(_lp, true); - wasm.weaveffi_dealloc(_lp, 4); - const _rd = new _BufReader(_takeBytes(wasm, _r, _len)); - const _out = _read_contacts_Contact(_rd); - _rd.end(); - return _out; - } - list() { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_contacts_ContactBook_list(this._handle, _lp, _err); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - const _len = new DataView(wasm.memory.buffer).getUint32(_lp, true); - wasm.weaveffi_dealloc(_lp, 4); - const _rd = new _BufReader(_takeBytes(wasm, _r, _len)); - const _out = (() => { const _n = _rd.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push(_read_contacts_Contact(_rd)); return _arr; })(); - _rd.end(); - return _out; - } - remove(id) { - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_contacts_ContactBook_remove(this._handle, BigInt(id), _err); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r !== 0; - } - count() { - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_contacts_ContactBook_count(this._handle, _err); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r; - } - } - - return { - _raw: wasm, - contacts: { - ContactBook: ContactBook, - }, - }; -} - -// END weaveffi_wasm.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__README_md.snap b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__README_md.snap index 4baa4afb..94882ac3 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__README_md.snap @@ -55,6 +55,7 @@ pointer as the last argument to each Wasm function. The error struct is size_t payload_len }`. Your Wasm module must export the following functions: +- `weaveffi_abi_version() -> i32`: the ABI revision the module implements; the loader refuses a mismatch - `weaveffi_alloc(size: i32) -> i32`: allocate `size` bytes in linear memory - `weaveffi_dealloc(ptr: i32, size: i32)`: release a `weaveffi_alloc` block - `weaveffi_error_clear(err_ptr: i32)`: clear and free error resources diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__weaveffi_wasm_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__weaveffi_wasm_d_ts.snap index 0c250fab..9384bfa3 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__weaveffi_wasm_d_ts.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__weaveffi_wasm_d_ts.snap @@ -62,6 +62,7 @@ export declare const Status: Readonly<{ /** Document is hidden from listings */ Archived: 2; }>; +export type Status = (typeof Status)[keyof typeof Status]; /** * An ordered collection of documents. diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__weaveffi_wasm_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__weaveffi_wasm_js.snap index 05683be5..4a168a55 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__weaveffi_wasm_js.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_docs_everywhere__weaveffi_wasm_js.snap @@ -17,6 +17,19 @@ source: crates/weaveffi-cli/tests/snapshots.rs // Buffered -> records, rich enums, optionals, lists, and maps cross // as one value buffer: i32 pointer + i32 length +// The ABI revision this glue was generated against. +const _ABI_VERSION = 1; + +function _checkAbiVersion(wasm) { + if (typeof wasm.weaveffi_abi_version !== 'function') { + throw new Error(`the loaded WeaveFFI module predates ABI versioning (this glue expects ABI revision ${_ABI_VERSION})`); + } + const found = wasm.weaveffi_abi_version() >>> 0; + if (found !== _ABI_VERSION) { + throw new Error(`WeaveFFI ABI mismatch: this glue expects revision ${_ABI_VERSION} but the loaded module reports revision ${found}`); + } +} + /** Base error for WeaveFFI failures: domain errors extend it, and it is * thrown directly for unknown codes, marshalling failures, and producer * panics. Carries the stable ABI `code`. */ @@ -353,6 +366,8 @@ export async function loadWeaveffiWasm(url) { const { instance } = await WebAssembly.instantiate(bytes, {}); const wasm = instance.exports; + _checkAbiVersion(wasm); + const _table = wasm.__indirect_function_table; let _nextCtxId = 1; diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__README_md.snap b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__README_md.snap new file mode 100644 index 00000000..eabfe41d --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__README_md.snap @@ -0,0 +1,531 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- + + + + +# WeaveFFI Wasm (experimental) + +This folder contains a minimal stub to help you load a `wasm32-unknown-unknown` build of your WeaveFFI library. + +Build (example): + +```bash +cargo build --target wasm32-unknown-unknown --release +``` + +Then serve the `.wasm` and use `weaveffi_wasm.js` to load it. + +## Complex Type Handling + +Wasm only supports numeric types natively (`i32`, `i64`, `f32`, `f64`). Complex types are encoded at the boundary as follows: + +### Records + +Records are **plain JavaScript objects**. A record crosses the boundary serialized in the WeaveFFI value-buffer format as one pointer + length pair in linear memory; the glue packs and unpacks it automatically, so there are no handles, no accessor functions, and nothing to free. + +### Enums + +C-style enums are passed as **`i32` values** corresponding to the variant's integer discriminant. Rich (algebraic) enums are plain objects tagged by variant name (`{ tag: "Circle", radius: 2 }`) and cross the boundary serialized in a value buffer, exactly like records. + +### Optionals + +Optional values map to **`null`** for the absent case. An optional crosses the boundary inside a value buffer as a one-byte presence flag followed by the value when present. The one exception is an optional interface, which stays a nullable object pointer (`0` = absent). + +### Lists and Maps + +Lists are JS arrays; maps are plain objects (a `Map` instance is also accepted on input). Both cross the boundary serialized in a value buffer as one pointer + length pair, recursing through element types. + +### Iterators + +`iter` functions return a **lazy JS iterator** (typed `IterableIterator`): each `next()` issues exactly one producer call, so iteration streams in constant memory. The producer handle is destroyed exactly once, on exhaustion or via `return()` when iteration stops early (a `for...of` loop calls `return()` automatically on `break` or `throw`). Abandoning an iterator without exhausting or closing it leaks the handle. + +### Callbacks and Listeners + +Each listener surfaces as a `register.../unregister...` pair. `register` takes a plain JS function and returns a numeric subscription id; `unregister` takes that id and stops delivery. Delivery is **synchronous and same-thread**: `wasm32-unknown-unknown` is single-threaded, so events fire only while a call into the module is on the stack (for example, a producer function that emits during its own execution). A producer that emits from a spawned thread cannot run on this target at all. + +Callback arguments are **borrowed for the duration of the callback**: strings, byte buffers, and buffered values (records, rich enums, optionals, lists, maps) are copied or decoded into JS values before your function runs, and interface arguments wrap producer-owned objects. Read what you need inside the callback and do not retain an interface wrapper or call `free()` on it. + +### Error Handling + +The generated JS wrappers automatically handle errors by passing an error +pointer as the last argument to each Wasm function. The error struct is +16 bytes on wasm32: `{ i32 code, char* message, uint8_t* payload_ptr, +size_t payload_len }`. Your Wasm module must export the following +functions: + +- `weaveffi_abi_version() -> i32`: the ABI revision the module implements; the loader refuses a mismatch +- `weaveffi_alloc(size: i32) -> i32`: allocate `size` bytes in linear memory +- `weaveffi_dealloc(ptr: i32, size: i32)`: release a `weaveffi_alloc` block +- `weaveffi_error_clear(err_ptr: i32)`: clear and free error resources +- `weaveffi_free_string(ptr: i32)`: free a producer-returned C string +- `weaveffi_free_bytes(ptr: i32, len: i32)`: free a producer-returned buffer + +Wrappers of functions declared `throws` raise the declaring module's typed +error class (a `WeaveFFIError` subclass with a per-code subclass, such as +`KeyNotFound`); every other wrapper raises the generic `WeaveFFIError` only +for producer panics and marshalling failures. When the matched error code +declares payload fields, the wrapper decodes them from the error's value +buffer and attaches them as properties on the thrown error. + +## API Reference + +### Module: `edge_cases` + +#### Error Domain: `EdgeErrorsError` + +Throwing wrappers in this module raise `EdgeErrorsError` (a `WeaveFFIError` subclass); each code below is its own subclass carrying the stable `code`. Payload fields are decoded from the error's value buffer and attached as properties on the thrown error. + +| Class | Code | Default Message | Payload Fields | +|-------|------|-----------------|----------------| +| `Busy` | `7` | Resource is busy | (none) | + +#### Functions + +##### `weaveffi_edge_cases_noop` + +No parameters and no result + +`weaveffi_edge_cases_noop() -> void` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| + +##### `weaveffi_edge_cases_select` + +Function named after a keyword whose parameters are all keywords + +`weaveffi_edge_cases_select(class: i32, from: i32, func: i32, i32, range: i32, i32, type_: i32) -> i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| `class` | `i32` | `i32` | native Wasm i32 | +| `from` | `string` | `i32` | NUL-terminated C string pointer | +| `func` | `[string]` | `i32, i32` | value buffer: ptr + len in linear memory | +| `range` | `i32?` | `i32, i32` | value buffer: ptr + len in linear memory | +| `type_` | `bool` | `i32` | 0 = false, 1 = true | +| _returns_ | `i32` | `i32` | native Wasm i32 | + +##### `weaveffi_edge_cases_nested_optional` + +Optional list of optional structs, the deepest legal nesting + +`weaveffi_edge_cases_nested_optional(items: i32, i32) -> i32, i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| `items` | `[Outer?]?` | `i32, i32` | value buffer: ptr + len in linear memory | +| _returns_ | `[Outer?]?` | `i32, i32` | value buffer: ptr + len in linear memory | + +##### `weaveffi_edge_cases_optional_params` + +`weaveffi_edge_cases_optional_params(text: i32, i32, count: i32, i32, outer: i32, i32, tags: i32, i32, blob: i32, i32, mode: i32, i32) -> i32, i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| `text` | `string?` | `i32, i32` | value buffer: ptr + len in linear memory | +| `count` | `i32?` | `i32, i32` | value buffer: ptr + len in linear memory | +| `outer` | `Outer?` | `i32, i32` | value buffer: ptr + len in linear memory | +| `tags` | `[string]?` | `i32, i32` | value buffer: ptr + len in linear memory | +| `blob` | `bytes?` | `i32, i32` | value buffer: ptr + len in linear memory | +| `mode` | `Mode?` | `i32, i32` | value buffer: ptr + len in linear memory | +| _returns_ | `Mode?` | `i32, i32` | value buffer: ptr + len in linear memory | + +##### `weaveffi_edge_cases_mutable_params` + +Write-back parameters + +`weaveffi_edge_cases_mutable_params(text: i32, blob: i32, i32) -> void` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| `text` | `string` | `i32` | NUL-terminated C string pointer | +| `blob` | `bytes` | `i32, i32` | ptr + len in linear memory | + +##### `weaveffi_edge_cases_make_widget` + +Factory for an interface with no constructor + +`weaveffi_edge_cases_make_widget(label: i32) -> i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| `label` | `string` | `i32` | NUL-terminated C string pointer | +| _returns_ | `Widget` | `i32` | opaque object pointer | + +##### `weaveffi_edge_cases_find_widget` + +Optional interface return + +`weaveffi_edge_cases_find_widget(id: i64) -> i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| `id` | `i64` | `i64` | native Wasm i64 | +| _returns_ | `Widget?` | `i32` | nullable object pointer, 0 = absent | + +##### `weaveffi_edge_cases_inspect_widget` + +Interface parameter, including an optional one + +`weaveffi_edge_cases_inspect_widget(widget: i32, fallback: i32) -> i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| `widget` | `Widget` | `i32` | opaque object pointer | +| `fallback` | `Widget?` | `i32` | nullable object pointer, 0 = absent | +| _returns_ | `string` | `i32` | NUL-terminated C string pointer | + +##### `weaveffi_edge_cases_typed_handle_param` + +`weaveffi_edge_cases_typed_handle_param(token: i32, raw: i64) -> i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| `token` | `handle` | `i32` | opaque pointer | +| `raw` | `handle` | `i64` | opaque 64-bit handle | +| _returns_ | `handle` | `i32` | opaque pointer | + +##### `weaveffi_edge_cases_string_stream` + +Iterator of strings + +`weaveffi_edge_cases_string_stream() -> i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| _returns_ | `iter` | `i32` | opaque iterator handle | + +##### `weaveffi_edge_cases_scalar_stream` + +Iterator of scalars + +`weaveffi_edge_cases_scalar_stream() -> i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| _returns_ | `iter` | `i32` | opaque iterator handle | + +##### `weaveffi_edge_cases_mode_stream` + +Iterator of enums + +`weaveffi_edge_cases_mode_stream() -> i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| _returns_ | `iter` | `i32` | opaque iterator handle | + +##### `weaveffi_edge_cases_async_void` + +Async with no result + +`weaveffi_edge_cases_async_void() -> void` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| + +##### `weaveffi_edge_cases_async_bytes` + +`weaveffi_edge_cases_async_bytes(size: i32) -> i32, i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| `size` | `u32` | `i32` | unsigned mapped to i32 | +| _returns_ | `bytes` | `i32, i32` | ptr + len in linear memory | + +##### `weaveffi_edge_cases_async_struct` + +`weaveffi_edge_cases_async_struct() -> i32, i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| _returns_ | `Outer` | `i32, i32` | value buffer: ptr + len in linear memory | + +##### `weaveffi_edge_cases_async_list` + +`weaveffi_edge_cases_async_list() -> i32, i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| _returns_ | `[Outer]` | `i32, i32` | value buffer: ptr + len in linear memory | + +##### `weaveffi_edge_cases_async_optional` + +`weaveffi_edge_cases_async_optional() -> i32, i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| _returns_ | `i64?` | `i32, i32` | value buffer: ptr + len in linear memory | + +##### `weaveffi_edge_cases_async_enum` + +`weaveffi_edge_cases_async_enum() -> i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| _returns_ | `Mode` | `i32` | variant discriminant | + +##### `weaveffi_edge_cases_async_keywords` + +`weaveffi_edge_cases_async_keywords(class: i32, import: i32) -> i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| `class` | `string` | `i32` | NUL-terminated C string pointer | +| `import` | `i32` | `i32` | native Wasm i32 | +| _returns_ | `string` | `i32` | NUL-terminated C string pointer | + +##### `weaveffi_edge_cases_keywords_roundtrip` + +`weaveffi_edge_cases_keywords_roundtrip(value: i32, i32) -> i32, i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| `value` | `Keywords` | `i32, i32` | value buffer: ptr + len in linear memory | +| _returns_ | `Keywords` | `i32, i32` | value buffer: ptr + len in linear memory | + +##### `weaveffi_edge_cases_legacy_value` + +**Deprecated:** Use Mode-based APIs + +`weaveffi_edge_cases_legacy_value(value: i32) -> i32, i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| `value` | `Legacy` | `i32` | variant discriminant | +| _returns_ | `Retired` | `i32, i32` | value buffer: ptr + len in linear memory | + +#### Interfaces + +##### `Widget` + +Interface with no constructor; instances come from make_widget + +Passed as an **opaque object pointer** (`i32`), wrapped by a JS class. Constructors return an owned handle; methods pass the handle as the implicit leading `self` argument; `free()` releases the handle via the destroy symbol. + +##### `weaveffi_edge_cases_Widget_refresh` + +Void method that can fail + +`weaveffi_edge_cases_Widget_refresh() -> void` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| + +##### `weaveffi_edge_cases_Widget_rename` + +`weaveffi_edge_cases_Widget_rename(class: i32, import: i32, i32) -> i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| `class` | `string` | `i32` | NUL-terminated C string pointer | +| `import` | `string?` | `i32, i32` | value buffer: ptr + len in linear memory | +| _returns_ | `string` | `i32` | NUL-terminated C string pointer | + +##### `weaveffi_edge_cases_Widget_fetch` + +Async method with a struct result + +`weaveffi_edge_cases_Widget_fetch(id: i64) -> i32, i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| `id` | `i64` | `i64` | native Wasm i64 | +| _returns_ | `Outer` | `i32, i32` | value buffer: ptr + len in linear memory | + +##### `weaveffi_edge_cases_Widget_old_name` + +**Deprecated:** Use rename instead + +`weaveffi_edge_cases_Widget_old_name() -> i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| _returns_ | `string` | `i32` | NUL-terminated C string pointer | + +##### `weaveffi_edge_cases_Widget_destroy` + +Releases the object reference. Called by the wrapper's `free()`. + +##### `Vault` + +Deprecated interface with a throwing constructor + +Passed as an **opaque object pointer** (`i32`), wrapped by a JS class. Constructors return an owned handle; methods pass the handle as the implicit leading `self` argument; `free()` releases the handle via the destroy symbol. + +##### `weaveffi_edge_cases_Vault_open` + +Fails when the vault is busy + +`weaveffi_edge_cases_Vault_open(path: i32) -> i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| `path` | `string` | `i32` | NUL-terminated C string pointer | +| _returns_ | `Vault` | `i32` | opaque object pointer | + +##### `weaveffi_edge_cases_Vault_peek` + +`weaveffi_edge_cases_Vault_peek() -> i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| _returns_ | `Widget?` | `i32` | nullable object pointer, 0 = absent | + +##### `weaveffi_edge_cases_Vault_probe` + +Throwing static + +`weaveffi_edge_cases_Vault_probe() -> i32` + +| Param | API Type | Wasm | Notes | +|-------|----------|------|-------| +| _returns_ | `bool` | `i32` | 0 = false, 1 = true | + +##### `weaveffi_edge_cases_Vault_destroy` + +Releases the object reference. Called by the wrapper's `free()`. + +#### Structs + +##### `Leaf` + +A plain JS object, serialized in a value buffer (`i32` ptr + `i32` len) at the boundary. Fields in declaration (wire) order: + +| Field | Type | +|-------|------| +| `weight` | `f32` | + +##### `Inner` + +Middle layer of a three-deep struct nesting + +A plain JS object, serialized in a value buffer (`i32` ptr + `i32` len) at the boundary. Fields in declaration (wire) order: + +| Field | Type | +|-------|------| +| `leaf` | `Leaf` | +| `leaves` | `[Leaf]` | + +##### `Outer` + +A plain JS object, serialized in a value buffer (`i32` ptr + `i32` len) at the boundary. Fields in declaration (wire) order: + +| Field | Type | +|-------|------| +| `inner` | `Inner` | +| `maybe_inner` | `Inner?` | +| `by_name` | `{string:Inner}` | +| `modes` | `[Mode]` | +| `matrix` | `[[i32]]` | +| `sparse` | `[i64?]` | +| `blob` | `bytes?` | +| `note` | `string?` | +| `by_id` | `{i64:string}` | +| `_hidden` | `bool` | +| `v2` | `u8` | + +##### `Keywords` + +Every field is a reserved word somewhere + +A plain JS object, serialized in a value buffer (`i32` ptr + `i32` len) at the boundary. Fields in declaration (wire) order: + +| Field | Type | +|-------|------| +| `class` | `i32` | +| `def` | `i32` | +| `import` | `i32` | +| `from` | `i32` | +| `func` | `i32` | +| `range` | `i32` | +| `register` | `i32` | +| `volatile` | `i32` | +| `new` | `i32` | +| `default` | `i32` | +| `end` | `i32` | +| `in` | `i32` | +| `is` | `i32` | +| `int` | `i32` | +| `var` | `i32` | +| `val` | `i32` | +| `self` | `i32` | +| `this` | `i32` | +| `object` | `i32` | +| `string` | `i32` | +| `case` | `i32` | +| `select` | `i32` | +| `switch` | `i32` | +| `void` | `i32` | +| `union` | `i32` | +| `namespace` | `i32` | +| `operator` | `i32` | +| `template` | `i32` | +| `package` | `i32` | +| `protocol` | `i32` | +| `guard` | `i32` | +| `defer` | `i32` | +| `inout` | `i32` | +| `internal` | `i32` | +| `when` | `i32` | +| `fun` | `i32` | +| `data` | `i32` | +| `late` | `i32` | +| `part` | `i32` | +| `out` | `i32` | +| `ref` | `i32` | +| `event` | `i32` | +| `base` | `i32` | +| `lambda` | `i32` | +| `yield` | `i32` | +| `nil` | `i32` | +| `null` | `i32` | +| `begin` | `i32` | +| `rescue` | `i32` | +| `module` | `i32` | +| `chan` | `i32` | +| `go` | `i32` | +| `let` | `i32` | +| `export` | `i32` | +| `delete` | `i32` | +| `typedef` | `i32` | +| `sizeof` | `i32` | +| `static` | `i32` | +| `super` | `i32` | + +##### `Retired` + +Deprecated struct + +A plain JS object, serialized in a value buffer (`i32` ptr + `i32` len) at the boundary. Fields in declaration (wire) order: + +| Field | Type | +|-------|------| +| `id` | `i64` | + +#### Enums + +##### `Legacy` + +Deprecated enum with non-contiguous values and keyword-like variants + +Passed as `i32` discriminant. + +| Variant | Value | +|---------|-------| +| `None` | `0` | +| `Default` | `5` | +| `Class` | `10` | + +##### `Mode` + +Passed as `i32` discriminant. + +| Variant | Value | +|---------|-------| +| `Fast` | `1` | +| `Safe` | `2` | + + diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_async_demo__package_json.snap b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__package_json.snap similarity index 100% rename from crates/weaveffi-cli/tests/snapshots/wasm_async_demo__package_json.snap rename to crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__package_json.snap diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__weaveffi_wasm_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__weaveffi_wasm_d_ts.snap new file mode 100644 index 00000000..b428cf9a --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__weaveffi_wasm_d_ts.snap @@ -0,0 +1,305 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from weaveffi.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate weaveffi.yml -o + +// Generated TypeScript declarations for WeaveFFI Wasm bindings + +/** Base error for WeaveFFI failures: domain errors extend it, and it is + * thrown directly for unknown codes, marshalling failures, and producer + * panics. Carries the stable ABI `code`. */ +export declare class WeaveFFIError extends Error { + constructor(code: number, message?: string); + code: number; +} + +/** Base error for the `edge_cases` module's error domain. */ +export declare class EdgeErrorsError extends WeaveFFIError { + static readonly Busy: typeof Busy; +} + +/** Resource is busy */ +export declare class Busy extends EdgeErrorsError { + constructor(message?: string); + static readonly CODE: 7; +} + +export interface Leaf { + weight: number; +} + +/** Middle layer of a three-deep struct nesting */ +export interface Inner { + leaf: Leaf; + leaves: Leaf[]; +} + +export interface Outer { + inner: Inner; + maybe_inner: Inner | null; + by_name: Record; + modes: 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 */ +export interface Retired { + id: bigint; +} + +/** Deprecated enum with non-contiguous values and keyword-like variants */ +export declare const Legacy: Readonly<{ + None: 0; + Default: 5; + Class: 10; +}>; +export type Legacy = (typeof Legacy)[keyof typeof Legacy]; + +export declare const Mode: Readonly<{ + Fast: 1; + Safe: 2; +}>; +export type Mode = (typeof Mode)[keyof typeof Mode]; + +/** Interface with no constructor; instances come from make_widget */ +export declare class Widget { + /** + * Void method that can fail + * @throws {EdgeErrorsError} on a domain error code + */ + refresh(): void; + /** + * @throws {WeaveFFIError} if the native call fails + */ + rename(class_: string, import_: string | null): string; + /** + * Async method with a struct result + * @throws {EdgeErrorsError} on a domain error code + */ + fetch(id: bigint): Promise; + /** + * @deprecated Use rename instead + * @throws {WeaveFFIError} if the native call fails + */ + oldName(): string; + /** Releases the producer-owned handle exactly once. */ + free(): void; +} + +/** Deprecated interface with a throwing constructor */ +export declare class Vault { + /** + * Fails when the vault is busy + * @throws {EdgeErrorsError} on a domain error code + */ + static open(path: string): Vault; + /** + * @throws {WeaveFFIError} if the native call fails + */ + peek(): Widget | null; + /** + * Throwing static + * @throws {EdgeErrorsError} on a domain error code + */ + static probe(): boolean; + /** Releases the producer-owned handle exactly once. */ + free(): void; +} + +export interface WeaveffiWasmModule { + _raw: WebAssembly.Exports; + edge_cases: { + /** + * No parameters and no result + * @throws {WeaveFFIError} if the native call fails + */ + noop(): void; + /** + * Function named after a keyword whose parameters are all keywords + * @throws {WeaveFFIError} if the native call fails + */ + select(class_: number, from: string, func: string[], range: number | null, type: boolean): number; + /** + * Optional list of optional structs, the deepest legal nesting + * @throws {WeaveFFIError} if the native call fails + */ + nestedOptional(items: (Outer | null)[] | null): (Outer | null)[] | null; + /** + * @throws {WeaveFFIError} if the native call fails + */ + optionalParams(text: string | null, count: number | null, outer: Outer | null, tags: string[] | null, blob: Uint8Array | null, mode: Mode | null): Mode | null; + /** + * Write-back parameters + * @throws {WeaveFFIError} if the native call fails + */ + mutableParams(text: string, blob: Uint8Array): void; + /** + * Factory for an interface with no constructor + * @throws {WeaveFFIError} if the native call fails + */ + makeWidget(label: string): Widget; + /** + * Optional interface return + * @throws {EdgeErrorsError} on a domain error code + */ + findWidget(id: bigint): Widget | null; + /** + * Interface parameter, including an optional one + * @throws {WeaveFFIError} if the native call fails + */ + inspectWidget(widget: Widget, fallback: Widget | null): string; + /** + * @throws {WeaveFFIError} if the native call fails + */ + typedHandleParam(token: number, raw: bigint): number; + /** + * Iterator of strings + * @returns A lazy iterator: one producer step per `next()` call. Exhaust it or call `return()` to release the producer handle (a `for...of` loop does both automatically); an abandoned iterator leaks the handle. + * @throws {WeaveFFIError} if the native call fails + */ + stringStream(): IterableIterator; + /** + * Iterator of scalars + * @returns A lazy iterator: one producer step per `next()` call. Exhaust it or call `return()` to release the producer handle (a `for...of` loop does both automatically); an abandoned iterator leaks the handle. + * @throws {WeaveFFIError} if the native call fails + */ + scalarStream(): IterableIterator; + /** + * Iterator of enums + * @returns A lazy iterator: one producer step per `next()` call. Exhaust it or call `return()` to release the producer handle (a `for...of` loop does both automatically); an abandoned iterator leaks the handle. + * @throws {WeaveFFIError} if the native call fails + */ + modeStream(): IterableIterator; + /** + * Async with no result + * @throws {WeaveFFIError} if the native call fails + */ + asyncVoid(): Promise; + /** + * @throws {WeaveFFIError} if the native call fails + */ + asyncBytes(size: number): Promise; + /** + * @throws {EdgeErrorsError} on a domain error code + */ + asyncStruct(): Promise; + /** + * @throws {EdgeErrorsError} on a domain error code + */ + asyncList(): Promise; + /** + * @throws {WeaveFFIError} if the native call fails + */ + asyncOptional(): Promise; + /** + * @throws {WeaveFFIError} if the native call fails + */ + asyncEnum(): Promise; + /** + * @throws {WeaveFFIError} if the native call fails + */ + asyncKeywords(class_: string, import_: number): Promise; + /** + * @throws {WeaveFFIError} if the native call fails + */ + keywordsRoundtrip(value: Keywords): Keywords; + /** + * @deprecated Use Mode-based APIs + * @throws {WeaveFFIError} if the native call fails + */ + legacyValue(value: Legacy): Retired; + /** + * Register a listener for the `OnBatch` callback. + * + * @returns A subscription id for `unregisterBatchListener()`. + */ + registerBatchListener(callback: (items: Outer[], raw: Uint8Array, first: Outer | null, mode: Mode) => void): number; + /** Unregister a listener previously registered with `registerBatchListener()`. */ + unregisterBatchListener(id: number): void; + /** + * Register a listener for the `OnTick` callback. + * + * @returns A subscription id for `unregisterTickListener()`. + */ + registerTickListener(callback: () => void): number; + /** Unregister a listener previously registered with `registerTickListener()`. */ + unregisterTickListener(id: number): void; + Widget: typeof Widget; + Vault: typeof Vault; + }; +} + +export function loadWeaveffiWasm(url: string): Promise; + +// END weaveffi_wasm.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__weaveffi_wasm_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__weaveffi_wasm_js.snap new file mode 100644 index 00000000..8f1a8876 --- /dev/null +++ b/crates/weaveffi-cli/tests/snapshots/wasm_edge_cases__weaveffi_wasm_js.snap @@ -0,0 +1,1147 @@ +--- +source: crates/weaveffi-cli/tests/snapshots.rs +--- +// Generated by WeaveFFI [VERSION] from weaveffi.yml +// DO NOT EDIT. Your changes will be overwritten. +// To regenerate: weaveffi generate weaveffi.yml -o + +// WeaveFFI Wasm bindings (auto-generated) +// +// Boundary conventions for a wasm32-unknown-unknown build: +// +// Objects -> i32 pointer into linear memory (0 = null/absent) +// Enums -> i32 discriminant value +// i64/u64 -> JavaScript BigInt +// Strings -> NUL-terminated UTF-8 (const char*); a single i32 pointer +// Bytes -> i32 data pointer + i32 length (out_len for returns) +// Buffered -> records, rich enums, optionals, lists, and maps cross +// as one value buffer: i32 pointer + i32 length + +// The ABI revision this glue was generated against. +const _ABI_VERSION = 1; + +function _checkAbiVersion(wasm) { + if (typeof wasm.weaveffi_abi_version !== 'function') { + throw new Error(`the loaded WeaveFFI module predates ABI versioning (this glue expects ABI revision ${_ABI_VERSION})`); + } + const found = wasm.weaveffi_abi_version() >>> 0; + if (found !== _ABI_VERSION) { + throw new Error(`WeaveFFI ABI mismatch: this glue expects revision ${_ABI_VERSION} but the loaded module reports revision ${found}`); + } +} + +/** Base error for WeaveFFI failures: domain errors extend it, and it is + * thrown directly for unknown codes, marshalling failures, and producer + * panics. Carries the stable ABI `code`. */ +export class WeaveFFIError extends Error { + constructor(code, message) { + super(message ? `WeaveFFI error ${code}: ${message}` : `WeaveFFI error ${code}`); + this.name = new.target.name; + this.code = code; + } +} + +/** Base error for the `edge_cases` module's error domain. */ +export class EdgeErrorsError extends WeaveFFIError {} + +// Resource is busy +export class Busy extends EdgeErrorsError { + constructor(message = "Resource is busy") { + super(7, message); + } +} +Busy.CODE = 7; +EdgeErrorsError.Busy = Busy; + +const _EDGE_ERRORS_ERROR_CODES = Object.freeze({ + 7: Busy, +}); + +// Build the EdgeErrorsError subclass matching `code`, or a generic +// WeaveFFIError for codes outside the domain (panics, marshalling). +function _edgeErrorsErrorFrom(wasm, code, message, payloadPtr, payloadLen) { + const _cls = _EDGE_ERRORS_ERROR_CODES[code]; + const _e = _cls ? (message ? new _cls(message) : new _cls()) : new WeaveFFIError(code, message); + return _e; +} + +const _enc = new TextEncoder(); +const _dec = new TextDecoder(); + +// Stage a JS string as a NUL-terminated C string in linear memory. +// Returns [ptr, size] (size includes the NUL); release with _free. +function _cstr(wasm, str) { + const bytes = _enc.encode(str); + const size = bytes.length + 1; + const ptr = wasm.weaveffi_alloc(size); + const mem = new Uint8Array(wasm.memory.buffer, ptr, size); + mem.set(bytes); + mem[bytes.length] = 0; + return [ptr, size]; +} + +// Read a NUL-terminated C string (0 => null). Does not free. +function _readCStr(wasm, ptr) { + if (ptr === 0) return null; + const mem = new Uint8Array(wasm.memory.buffer); + let end = ptr; + while (mem[end] !== 0) end++; + return _dec.decode(mem.subarray(ptr, end)); +} + +// Read then free a producer-owned C string. +function _takeCStr(wasm, ptr) { + const s = _readCStr(wasm, ptr); + if (ptr !== 0) wasm.weaveffi_free_string(ptr); + return s; +} + +// Stage a byte buffer (or an encoded value buffer); returns [ptr, len]; +// release with weaveffi_dealloc(ptr, len). +function _bytes(wasm, data) { + const u8 = data instanceof Uint8Array ? data : new Uint8Array(data); + const ptr = wasm.weaveffi_alloc(u8.length); + if (u8.length) new Uint8Array(wasm.memory.buffer, ptr, u8.length).set(u8); + return [ptr, u8.length]; +} + +// Copy then free a producer-owned byte (or value) buffer. +function _takeBytes(wasm, ptr, len) { + if (ptr === 0 || len === 0) return new Uint8Array(0); + const copy = new Uint8Array(wasm.memory.buffer, ptr, len).slice(); + wasm.weaveffi_free_bytes(ptr, len); + return copy; +} + +// Growable little-endian byte writer implementing the WeaveFFI value-buffer +// wire format: values are packed back to back with no alignment; lengths and +// counts are u32; strings are u32 byte length + UTF-8 bytes (no NUL). +class _BufWriter { + constructor() { + this._u8 = new Uint8Array(64); + this._dv = new DataView(this._u8.buffer); + this._len = 0; + } + _need(n) { + if (this._len + n <= this._u8.length) return; + let cap = this._u8.length * 2; + while (cap < this._len + n) cap *= 2; + const u8 = new Uint8Array(cap); + u8.set(this._u8.subarray(0, this._len)); + this._u8 = u8; + this._dv = new DataView(u8.buffer); + } + bool(v) { this._need(1); this._dv.setUint8(this._len, v ? 1 : 0); this._len += 1; } + i8(v) { this._need(1); this._dv.setInt8(this._len, v); this._len += 1; } + u8(v) { this._need(1); this._dv.setUint8(this._len, v); this._len += 1; } + i16(v) { this._need(2); this._dv.setInt16(this._len, v, true); this._len += 2; } + u16(v) { this._need(2); this._dv.setUint16(this._len, v, true); this._len += 2; } + i32(v) { this._need(4); this._dv.setInt32(this._len, v, true); this._len += 4; } + u32(v) { this._need(4); this._dv.setUint32(this._len, v, true); this._len += 4; } + i64(v) { this._need(8); this._dv.setBigInt64(this._len, BigInt(v), true); this._len += 8; } + u64(v) { this._need(8); this._dv.setBigUint64(this._len, BigInt(v), true); this._len += 8; } + f32(v) { this._need(4); this._dv.setFloat32(this._len, v, true); this._len += 4; } + f64(v) { this._need(8); this._dv.setFloat64(this._len, v, true); this._len += 8; } + len(n) { this.u32(n); } + flag(present) { this.u8(present ? 1 : 0); } + str(v) { + const b = _enc.encode(v); + this.len(b.length); + this._need(b.length); + this._u8.set(b, this._len); + this._len += b.length; + } + bytes(v) { + const b = v instanceof Uint8Array ? v : new Uint8Array(v); + this.len(b.length); + this._need(b.length); + this._u8.set(b, this._len); + this._len += b.length; + } + finish() { return this._u8.subarray(0, this._len); } +} + +const _bufDec = new TextDecoder('utf-8', { fatal: true }); + +// Strict little-endian reader for the WeaveFFI value-buffer wire format. A +// malformed buffer (truncation, invalid bool or flag bytes, an oversized +// length prefix, invalid UTF-8, trailing bytes) is a producer/consumer +// contract violation and throws the generic brand error; code -3 marks a +// consumer-side marshalling failure. +class _BufReader { + constructor(bytes) { + this._u8 = bytes; + this._dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + this._pos = 0; + } + _bad(what) { + throw new WeaveFFIError(-3, 'malformed value buffer: ' + what); + } + _take(n, what) { + if (this._pos + n > this._u8.length) this._bad('truncated ' + what); + const at = this._pos; + this._pos += n; + return at; + } + bool() { + const b = this._u8[this._take(1, 'bool')]; + if (b > 1) this._bad('bool byte out of range'); + return b === 1; + } + i8() { return this._dv.getInt8(this._take(1, 'i8')); } + u8() { return this._u8[this._take(1, 'u8')]; } + i16() { return this._dv.getInt16(this._take(2, 'i16'), true); } + u16() { return this._dv.getUint16(this._take(2, 'u16'), true); } + i32() { return this._dv.getInt32(this._take(4, 'i32'), true); } + u32() { return this._dv.getUint32(this._take(4, 'u32'), true); } + i64() { return this._dv.getBigInt64(this._take(8, 'i64'), true); } + u64() { return this._dv.getBigUint64(this._take(8, 'u64'), true); } + f32() { return this._dv.getFloat32(this._take(4, 'f32'), true); } + f64() { return this._dv.getFloat64(this._take(8, 'f64'), true); } + len() { + const n = this.u32(); + if (n > this._u8.length - this._pos) this._bad('length prefix exceeds remaining buffer'); + return n; + } + flag() { + const b = this._u8[this._take(1, 'option flag')]; + if (b > 1) this._bad('option flag byte out of range'); + return b === 1; + } + str() { + const n = this.len(); + const at = this._take(n, 'string bytes'); + try { + return _bufDec.decode(this._u8.subarray(at, at + n)); + } catch (e) { + this._bad('string is not valid UTF-8'); + } + } + bytes() { + const n = this.len(); + const at = this._take(n, 'byte buffer'); + return this._u8.subarray(at, at + n).slice(); + } + end() { + if (this._pos !== this._u8.length) this._bad('trailing bytes after value'); + } +} + +// Allocate a zeroed 16-byte error slot: +// { i32 code, char* message, uint8_t* payload_ptr, size_t payload_len }. +function _allocErr(wasm) { + const ptr = wasm.weaveffi_alloc(16); + new Uint8Array(wasm.memory.buffer, ptr, 16).fill(0); + return ptr; +} + +// Throw (and free the slot) if the error slot carries a non-zero code. +// Non-throwing wrappers route here: a non-zero code can only be a +// producer panic or a marshalling failure, surfaced as the generic +// WeaveFFIError. +function _checkErr(wasm, errPtr) { + const dv = new DataView(wasm.memory.buffer); + const code = dv.getInt32(errPtr, true); + if (code !== 0) { + const msgPtr = dv.getUint32(errPtr + 4, true); + const msg = _readCStr(wasm, msgPtr) || ''; + wasm.weaveffi_error_clear(errPtr); + wasm.weaveffi_dealloc(errPtr, 16); + throw new WeaveFFIError(code, msg); + } +} + +// Release an error slot on the success path. +function _freeErr(wasm, errPtr) { + wasm.weaveffi_dealloc(errPtr, 16); +} + +// Throw the `EdgeErrorsError` domain error (and free the slot) if the error slot +// carries a non-zero code. +function _checkEdgeErrorsError(wasm, errPtr) { + const dv = new DataView(wasm.memory.buffer); + const code = dv.getInt32(errPtr, true); + if (code !== 0) { + const msg = _readCStr(wasm, dv.getUint32(errPtr + 4, true)) || ''; + const _e = _edgeErrorsErrorFrom(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)); + wasm.weaveffi_error_clear(errPtr); + wasm.weaveffi_dealloc(errPtr, 16); + throw _e; + } +} + +// Throw if a heap-boxed (consumer-owned) error carries a non-zero +// code. Used by async callbacks: the fields are read, then the box +// is released with weaveffi_error_free before throwing. +// `mkErr` maps domain codes (and decodes payload fields) for +// throwing callables; without it the generic WeaveFFIError is thrown. +function _checkErrRef(wasm, errPtr, mkErr) { + const dv = new DataView(wasm.memory.buffer); + const code = dv.getInt32(errPtr, true); + if (code === 0) { wasm.weaveffi_error_free(errPtr); return; } + const msg = _readCStr(wasm, dv.getUint32(errPtr + 4, true)) || ''; + const err = mkErr ? mkErr(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)) : new WeaveFFIError(code, msg); + wasm.weaveffi_error_free(errPtr); + throw err; +} + +// Serialize a `edge_cases.Leaf` record into the value-buffer wire format. +function _write_edge_cases_Leaf(w, v) { + w.f32(v.weight); +} + +// Decode a `edge_cases.Leaf` record from the value-buffer wire format. +function _read_edge_cases_Leaf(r) { + const v = {}; + v.weight = r.f32(); + return v; +} + +// Serialize a `edge_cases.Inner` record into the value-buffer wire format. +function _write_edge_cases_Inner(w, v) { + _write_edge_cases_Leaf(w, v.leaf); + const _a1 = v.leaves || []; + w.len(_a1.length); + for (const _e1 of _a1) { + _write_edge_cases_Leaf(w, _e1); + } +} + +// Decode a `edge_cases.Inner` record from the value-buffer wire format. +function _read_edge_cases_Inner(r) { + const v = {}; + v.leaf = _read_edge_cases_Leaf(r); + v.leaves = (() => { const _n = r.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push(_read_edge_cases_Leaf(r)); return _arr; })(); + return v; +} + +// Serialize a `edge_cases.Outer` record into the value-buffer wire format. +function _write_edge_cases_Outer(w, v) { + _write_edge_cases_Inner(w, v.inner); + if (v.maybe_inner === null || v.maybe_inner === undefined) { + w.flag(false); + } else { + w.flag(true); + _write_edge_cases_Inner(w, v.maybe_inner); + } + const _s1 = v.by_name || {}; + const _m1 = _s1 instanceof Map ? [..._s1.entries()] : Object.entries(_s1); + w.len(_m1.length); + for (const [_k1, _v1] of _m1) { + w.str(_k1); + _write_edge_cases_Inner(w, _v1); + } + const _a2 = v.modes || []; + w.len(_a2.length); + for (const _e2 of _a2) { + w.i32(_e2); + } + const _a3 = v.matrix || []; + w.len(_a3.length); + for (const _e3 of _a3) { + const _a4 = _e3 || []; + w.len(_a4.length); + for (const _e4 of _a4) { + w.i32(_e4); + } + } + const _a5 = v.sparse || []; + w.len(_a5.length); + for (const _e5 of _a5) { + if (_e5 === null || _e5 === undefined) { + w.flag(false); + } else { + w.flag(true); + w.i64(_e5); + } + } + if (v.blob === null || v.blob === undefined) { + w.flag(false); + } else { + w.flag(true); + w.bytes(v.blob); + } + if (v.note === null || v.note === undefined) { + w.flag(false); + } else { + w.flag(true); + w.str(v.note); + } + const _s6 = v.by_id || {}; + const _m6 = _s6 instanceof Map ? [..._s6.entries()] : Object.entries(_s6); + w.len(_m6.length); + for (const [_k6, _v6] of _m6) { + w.i64(_k6); + w.str(_v6); + } + w.bool(v._hidden); + w.u8(v.v2); +} + +// Decode a `edge_cases.Outer` record from the value-buffer wire format. +function _read_edge_cases_Outer(r) { + const v = {}; + v.inner = _read_edge_cases_Inner(r); + v.maybe_inner = (r.flag() ? _read_edge_cases_Inner(r) : null); + v.by_name = (() => { const _n = r.len(); const _obj = {}; for (let _i = 0; _i < _n; _i++) { const _k = r.str(); _obj[_k] = _read_edge_cases_Inner(r); } return _obj; })(); + v.modes = (() => { const _n = r.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push(r.i32()); return _arr; })(); + v.matrix = (() => { const _n = r.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push((() => { const _n = r.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push(r.i32()); return _arr; })()); return _arr; })(); + v.sparse = (() => { const _n = r.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push((r.flag() ? r.i64() : null)); return _arr; })(); + v.blob = (r.flag() ? r.bytes() : null); + v.note = (r.flag() ? r.str() : null); + v.by_id = (() => { const _n = r.len(); const _obj = {}; for (let _i = 0; _i < _n; _i++) { const _k = r.i64(); _obj[_k] = r.str(); } return _obj; })(); + v._hidden = r.bool(); + v.v2 = r.u8(); + return v; +} + +// Serialize a `edge_cases.Keywords` record into the value-buffer wire format. +function _write_edge_cases_Keywords(w, v) { + w.i32(v.class); + w.i32(v.def); + w.i32(v.import); + w.i32(v.from); + w.i32(v.func); + w.i32(v.range); + w.i32(v.register); + w.i32(v.volatile); + w.i32(v.new); + w.i32(v.default); + w.i32(v.end); + w.i32(v.in); + w.i32(v.is); + w.i32(v.int); + w.i32(v.var); + w.i32(v.val); + w.i32(v.self); + w.i32(v.this); + w.i32(v.object); + w.i32(v.string); + w.i32(v.case); + w.i32(v.select); + w.i32(v.switch); + w.i32(v.void); + w.i32(v.union); + w.i32(v.namespace); + w.i32(v.operator); + w.i32(v.template); + w.i32(v.package); + w.i32(v.protocol); + w.i32(v.guard); + w.i32(v.defer); + w.i32(v.inout); + w.i32(v.internal); + w.i32(v.when); + w.i32(v.fun); + w.i32(v.data); + w.i32(v.late); + w.i32(v.part); + w.i32(v.out); + w.i32(v.ref); + w.i32(v.event); + w.i32(v.base); + w.i32(v.lambda); + w.i32(v.yield); + w.i32(v.nil); + w.i32(v.null); + w.i32(v.begin); + w.i32(v.rescue); + w.i32(v.module); + w.i32(v.chan); + w.i32(v.go); + w.i32(v.let); + w.i32(v.export); + w.i32(v.delete); + w.i32(v.typedef); + w.i32(v.sizeof); + w.i32(v.static); + w.i32(v.super); +} + +// Decode a `edge_cases.Keywords` record from the value-buffer wire format. +function _read_edge_cases_Keywords(r) { + const v = {}; + v.class = r.i32(); + v.def = r.i32(); + v.import = r.i32(); + v.from = r.i32(); + v.func = r.i32(); + v.range = r.i32(); + v.register = r.i32(); + v.volatile = r.i32(); + v.new = r.i32(); + v.default = r.i32(); + v.end = r.i32(); + v.in = r.i32(); + v.is = r.i32(); + v.int = r.i32(); + v.var = r.i32(); + v.val = r.i32(); + v.self = r.i32(); + v.this = r.i32(); + v.object = r.i32(); + v.string = r.i32(); + v.case = r.i32(); + v.select = r.i32(); + v.switch = r.i32(); + v.void = r.i32(); + v.union = r.i32(); + v.namespace = r.i32(); + v.operator = r.i32(); + v.template = r.i32(); + v.package = r.i32(); + v.protocol = r.i32(); + v.guard = r.i32(); + v.defer = r.i32(); + v.inout = r.i32(); + v.internal = r.i32(); + v.when = r.i32(); + v.fun = r.i32(); + v.data = r.i32(); + v.late = r.i32(); + v.part = r.i32(); + v.out = r.i32(); + v.ref = r.i32(); + v.event = r.i32(); + v.base = r.i32(); + v.lambda = r.i32(); + v.yield = r.i32(); + v.nil = r.i32(); + v.null = r.i32(); + v.begin = r.i32(); + v.rescue = r.i32(); + v.module = r.i32(); + v.chan = r.i32(); + v.go = r.i32(); + v.let = r.i32(); + v.export = r.i32(); + v.delete = r.i32(); + v.typedef = r.i32(); + v.sizeof = r.i32(); + v.static = r.i32(); + v.super = r.i32(); + return v; +} + +// Serialize a `edge_cases.Retired` record into the value-buffer wire format. +function _write_edge_cases_Retired(w, v) { + w.i64(v.id); +} + +// Decode a `edge_cases.Retired` record from the value-buffer wire format. +function _read_edge_cases_Retired(r) { + const v = {}; + v.id = r.i64(); + return v; +} + +// Lazy wrapper over a producer iterator handle, implementing the JS +// iterator protocol: each next() issues exactly one producer next call +// and yields one converted element, so iteration streams in constant +// memory. The handle is destroyed exactly once: eagerly on exhaustion, +// on a next error, or from return() when iteration stops early (a +// for...of loop calls return() automatically on break or throw). +// Abandoning an iterator without exhausting or closing it leaks the +// producer handle: JS has no finalization hook that is reliable across +// every target this loader supports. +class _WeaveFFIIterator { + constructor(wasm, handle, slotSize, callNext, destroy, check, decode) { + this._wasm = wasm; + this._handle = handle; + this._slotSize = slotSize; + this._callNext = callNext; + this._destroyFn = destroy; + this._check = check; + this._decode = decode; + this._slot = wasm.weaveffi_alloc(slotSize); + } + // Destroy the handle and release the element slot exactly once. + _close() { + if (this._handle === 0) return; + this._destroyFn(this._handle); + this._handle = 0; + this._wasm.weaveffi_dealloc(this._slot, this._slotSize); + this._slot = 0; + } + next() { + if (this._handle === 0) return { done: true, value: undefined }; + const wasm = this._wasm; + const _err = _allocErr(wasm); + let _has; + try { + _has = this._callNext(this._handle, this._slot, _err); + // Throws (and releases the slot) on a non-zero code. + this._check(wasm, _err); + } catch (e) { + this._close(); + throw e; + } + _freeErr(wasm, _err); + if (_has === 0) { + this._close(); + return { done: true, value: undefined }; + } + return { done: false, value: this._decode(wasm, this._slot) }; + } + // Early-exit cleanup; for...of calls this on break/throw. + return(value) { + this._close(); + return { done: true, value }; + } + [Symbol.iterator]() { + return this; + } +} + +function _registerTrampoline(table, paramTypes, handler) { + const idx = table.grow(1); + table.set(idx, new WebAssembly.Function( + { parameters: paramTypes, results: [] }, + handler + )); + return idx; +} + +export const Legacy = Object.freeze({ + None: 0, + Default: 5, + Class: 10, +}); + +export const Mode = Object.freeze({ + Fast: 1, + Safe: 2, +}); + +/** + * Load a WeaveFFI Wasm module from the given URL. + * + * @param {string} url - URL to the `.wasm` file. + * @returns {Promise} The API bindings. + * + * Exported functions follow the C ABI naming convention: + * weaveffi_{module}_{function}(params...) -> result + * + * @example + * const api = await loadWeaveffiWasm('lib.wasm'); + * + * // Primitive: plain numbers in, number out. + * const sum = api.math.add(1, 2); + * + * // Record: plain objects in and out (serialized automatically). + * const person = api.contacts.create({ name: 'Ada', age: 36 }); + * + * // Enum: pass the integer discriminant. + * api.ui.set_color(0); // 0 = first variant + * + * // Optional: pass null to omit, a value to provide. + * api.config.set_timeout(5000); // present + * api.config.set_timeout(null); // absent + * + * // List/Map: pass arrays/objects; receive arrays/objects. + * const names = api.data.all_names(); + */ +export async function loadWeaveffiWasm(url) { + const response = await fetch(url); + const bytes = await response.arrayBuffer(); + const { instance } = await WebAssembly.instantiate(bytes, {}); + const wasm = instance.exports; + + _checkAbiVersion(wasm); + + const _table = wasm.__indirect_function_table; + + let _nextCtxId = 1; + const _asyncContexts = new Map(); + + function _asyncHandler(ctxId, errPtr, ...results) { + const ctx = _asyncContexts.get(ctxId); + if (!ctx) return; + _asyncContexts.delete(ctxId); + try { + if (errPtr !== 0) _checkErrRef(wasm, errPtr, ctx.mkErr); + ctx.resolve(ctx.unwrap ? ctx.unwrap(wasm, ...results) : results[0]); + } catch (e) { + ctx.reject(e); + } + } + + const _cbPtr_i32_i32 = _registerTrampoline(_table, ['i32', 'i32'], _asyncHandler); + const _cbPtr_i32_i32_i32_i32 = _registerTrampoline(_table, ['i32', 'i32', 'i32', 'i32'], _asyncHandler); + const _cbPtr_i32_i32_i32 = _registerTrampoline(_table, ['i32', 'i32', 'i32'], _asyncHandler); + + // Listener subscriptions, keyed by the context id the loader + // threads through the C ABI's void* context slot. Each entry + // holds the JS callback and the producer's subscription id. + let _nextLsnId = 1; + const _listeners = new Map(); + + const _lsnPtr_weaveffi_edge_cases_OnBatch_fn = _registerTrampoline(_table, ['i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32', 'i32'], (a0, a1, a2, a3, a4, a5, a6, _ctx) => { + const _l = _listeners.get(_ctx); + if (_l === undefined) return; + const _p0_b = (a0 === 0 || a1 === 0) ? new Uint8Array(0) : new Uint8Array(wasm.memory.buffer, a0, a1).slice(); + const _p0_r = new _BufReader(_p0_b); + const _p0 = (() => { const _n = _p0_r.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push(_read_edge_cases_Outer(_p0_r)); return _arr; })(); + _p0_r.end(); + const _p1 = (a2 === 0 || a3 === 0) ? new Uint8Array(0) : new Uint8Array(wasm.memory.buffer, a2, a3).slice(); + const _p2_b = (a4 === 0 || a5 === 0) ? new Uint8Array(0) : new Uint8Array(wasm.memory.buffer, a4, a5).slice(); + const _p2_r = new _BufReader(_p2_b); + const _p2 = (_p2_r.flag() ? _read_edge_cases_Outer(_p2_r) : null); + _p2_r.end(); + const _p3 = a6; + _l.callback(_p0, _p1, _p2, _p3); + }); + const _lsnPtr_weaveffi_edge_cases_OnTick_fn = _registerTrampoline(_table, ['i32'], (_ctx) => { + const _l = _listeners.get(_ctx); + if (_l === undefined) return; + _l.callback(); + }); + + // Interface with no constructor; instances come from make_widget + class Widget { + static _wrap(handle) { + const _o = Object.create(Widget.prototype); + _o._handle = handle; + return _o; + } + free() { + if (this._handle !== 0) { + wasm.weaveffi_edge_cases_Widget_destroy(this._handle); + this._handle = 0; + } + } + refresh() { + const _err = _allocErr(wasm); + wasm.weaveffi_edge_cases_Widget_refresh(this._handle, _err); + _checkEdgeErrorsError(wasm, _err); + _freeErr(wasm, _err); + } + rename(class_, import_) { + const [a0_p, a0_s] = _cstr(wasm, class_); + const a1_w = new _BufWriter(); + if (import_ === null || import_ === undefined) { + a1_w.flag(false); + } else { + a1_w.flag(true); + a1_w.str(import_); + } + const [a1_p, a1_l] = _bytes(wasm, a1_w.finish()); + const _err = _allocErr(wasm); + const _r = wasm.weaveffi_edge_cases_Widget_rename(this._handle, a0_p, a1_p, a1_l, _err); + wasm.weaveffi_dealloc(a0_p, a0_s); + wasm.weaveffi_dealloc(a1_p, a1_l); + _checkErr(wasm, _err); + _freeErr(wasm, _err); + return _takeCStr(wasm, _r); + } + fetch(id) { + return new Promise((resolve, reject) => { + const ctxId = _nextCtxId++; + _asyncContexts.set(ctxId, { resolve, reject, mkErr: _edgeErrorsErrorFrom, unwrap: (w, ptr, len) => { + const _b = ptr === 0 || len === 0 ? new Uint8Array(0) : new Uint8Array(w.memory.buffer, ptr, len).slice(); + if (ptr !== 0) w.weaveffi_free_bytes(ptr, len); + const _rd = new _BufReader(_b); + const _v = _read_edge_cases_Outer(_rd); + _rd.end(); + return _v; + } }); + wasm.weaveffi_edge_cases_Widget_fetch_async(this._handle, BigInt(id), _cbPtr_i32_i32_i32_i32, ctxId); + }); + } + /** @deprecated Use rename instead */ + oldName() { + const _err = _allocErr(wasm); + const _r = wasm.weaveffi_edge_cases_Widget_old_name(this._handle, _err); + _checkErr(wasm, _err); + _freeErr(wasm, _err); + return _takeCStr(wasm, _r); + } + } + + // Deprecated interface with a throwing constructor + class Vault { + static _wrap(handle) { + const _o = Object.create(Vault.prototype); + _o._handle = handle; + return _o; + } + free() { + if (this._handle !== 0) { + wasm.weaveffi_edge_cases_Vault_destroy(this._handle); + this._handle = 0; + } + } + static open(path) { + const [a0_p, a0_s] = _cstr(wasm, path); + const _err = _allocErr(wasm); + const _r = wasm.weaveffi_edge_cases_Vault_open(a0_p, _err); + wasm.weaveffi_dealloc(a0_p, a0_s); + _checkEdgeErrorsError(wasm, _err); + _freeErr(wasm, _err); + return Vault._wrap(_r); + } + peek() { + const _err = _allocErr(wasm); + const _r = wasm.weaveffi_edge_cases_Vault_peek(this._handle, _err); + _checkErr(wasm, _err); + _freeErr(wasm, _err); + return _r === 0 ? null : Widget._wrap(_r); + } + static probe() { + const _err = _allocErr(wasm); + const _r = wasm.weaveffi_edge_cases_Vault_probe(_err); + _checkEdgeErrorsError(wasm, _err); + _freeErr(wasm, _err); + return _r !== 0; + } + } + + return { + _raw: wasm, + edge_cases: { + noop() { + const _err = _allocErr(wasm); + wasm.weaveffi_edge_cases_noop(_err); + _checkErr(wasm, _err); + _freeErr(wasm, _err); + }, + select(class_, from, func, range, type) { + const [a1_p, a1_s] = _cstr(wasm, from); + const a2_w = new _BufWriter(); + const _a1 = func || []; + a2_w.len(_a1.length); + for (const _e1 of _a1) { + a2_w.str(_e1); + } + const [a2_p, a2_l] = _bytes(wasm, a2_w.finish()); + const a3_w = new _BufWriter(); + if (range === null || range === undefined) { + a3_w.flag(false); + } else { + a3_w.flag(true); + a3_w.i32(range); + } + const [a3_p, a3_l] = _bytes(wasm, a3_w.finish()); + const _err = _allocErr(wasm); + const _r = wasm.weaveffi_edge_cases_select(class_, a1_p, a2_p, a2_l, a3_p, a3_l, type ? 1 : 0, _err); + wasm.weaveffi_dealloc(a1_p, a1_s); + wasm.weaveffi_dealloc(a2_p, a2_l); + wasm.weaveffi_dealloc(a3_p, a3_l); + _checkErr(wasm, _err); + _freeErr(wasm, _err); + return _r; + }, + nestedOptional(items) { + const a0_w = new _BufWriter(); + if (items === null || items === undefined) { + a0_w.flag(false); + } else { + a0_w.flag(true); + const _a1 = items || []; + a0_w.len(_a1.length); + for (const _e1 of _a1) { + if (_e1 === null || _e1 === undefined) { + a0_w.flag(false); + } else { + a0_w.flag(true); + _write_edge_cases_Outer(a0_w, _e1); + } + } + } + const [a0_p, a0_l] = _bytes(wasm, a0_w.finish()); + const _lp = wasm.weaveffi_alloc(4); + const _err = _allocErr(wasm); + const _r = wasm.weaveffi_edge_cases_nested_optional(a0_p, a0_l, _lp, _err); + wasm.weaveffi_dealloc(a0_p, a0_l); + _checkErr(wasm, _err); + _freeErr(wasm, _err); + const _len = new DataView(wasm.memory.buffer).getUint32(_lp, true); + wasm.weaveffi_dealloc(_lp, 4); + const _rd = new _BufReader(_takeBytes(wasm, _r, _len)); + const _out = (_rd.flag() ? (() => { const _n = _rd.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push((_rd.flag() ? _read_edge_cases_Outer(_rd) : null)); return _arr; })() : null); + _rd.end(); + return _out; + }, + optionalParams(text, count, outer, tags, blob, mode) { + const a0_w = new _BufWriter(); + if (text === null || text === undefined) { + a0_w.flag(false); + } else { + a0_w.flag(true); + a0_w.str(text); + } + const [a0_p, a0_l] = _bytes(wasm, a0_w.finish()); + const a1_w = new _BufWriter(); + if (count === null || count === undefined) { + a1_w.flag(false); + } else { + a1_w.flag(true); + a1_w.i32(count); + } + const [a1_p, a1_l] = _bytes(wasm, a1_w.finish()); + const a2_w = new _BufWriter(); + if (outer === null || outer === undefined) { + a2_w.flag(false); + } else { + a2_w.flag(true); + _write_edge_cases_Outer(a2_w, outer); + } + const [a2_p, a2_l] = _bytes(wasm, a2_w.finish()); + const a3_w = new _BufWriter(); + if (tags === null || tags === undefined) { + a3_w.flag(false); + } else { + a3_w.flag(true); + const _a1 = tags || []; + a3_w.len(_a1.length); + for (const _e1 of _a1) { + a3_w.str(_e1); + } + } + const [a3_p, a3_l] = _bytes(wasm, a3_w.finish()); + const a4_w = new _BufWriter(); + if (blob === null || blob === undefined) { + a4_w.flag(false); + } else { + a4_w.flag(true); + a4_w.bytes(blob); + } + const [a4_p, a4_l] = _bytes(wasm, a4_w.finish()); + const a5_w = new _BufWriter(); + if (mode === null || mode === undefined) { + a5_w.flag(false); + } else { + a5_w.flag(true); + a5_w.i32(mode); + } + const [a5_p, a5_l] = _bytes(wasm, a5_w.finish()); + const _lp = wasm.weaveffi_alloc(4); + const _err = _allocErr(wasm); + const _r = wasm.weaveffi_edge_cases_optional_params(a0_p, a0_l, a1_p, a1_l, a2_p, a2_l, a3_p, a3_l, a4_p, a4_l, a5_p, a5_l, _lp, _err); + wasm.weaveffi_dealloc(a0_p, a0_l); + wasm.weaveffi_dealloc(a1_p, a1_l); + wasm.weaveffi_dealloc(a2_p, a2_l); + wasm.weaveffi_dealloc(a3_p, a3_l); + wasm.weaveffi_dealloc(a4_p, a4_l); + wasm.weaveffi_dealloc(a5_p, a5_l); + _checkErr(wasm, _err); + _freeErr(wasm, _err); + const _len = new DataView(wasm.memory.buffer).getUint32(_lp, true); + wasm.weaveffi_dealloc(_lp, 4); + const _rd = new _BufReader(_takeBytes(wasm, _r, _len)); + const _out = (_rd.flag() ? _rd.i32() : null); + _rd.end(); + return _out; + }, + mutableParams(text, blob) { + const [a0_p, a0_s] = _cstr(wasm, text); + const [a1_p, a1_l] = _bytes(wasm, blob); + const _err = _allocErr(wasm); + wasm.weaveffi_edge_cases_mutable_params(a0_p, a1_p, a1_l, _err); + wasm.weaveffi_dealloc(a0_p, a0_s); + wasm.weaveffi_dealloc(a1_p, a1_l); + _checkErr(wasm, _err); + _freeErr(wasm, _err); + }, + makeWidget(label) { + const [a0_p, a0_s] = _cstr(wasm, label); + const _err = _allocErr(wasm); + const _r = wasm.weaveffi_edge_cases_make_widget(a0_p, _err); + wasm.weaveffi_dealloc(a0_p, a0_s); + _checkErr(wasm, _err); + _freeErr(wasm, _err); + return Widget._wrap(_r); + }, + findWidget(id) { + const _err = _allocErr(wasm); + const _r = wasm.weaveffi_edge_cases_find_widget(BigInt(id), _err); + _checkEdgeErrorsError(wasm, _err); + _freeErr(wasm, _err); + return _r === 0 ? null : Widget._wrap(_r); + }, + inspectWidget(widget, fallback) { + const _err = _allocErr(wasm); + const _r = wasm.weaveffi_edge_cases_inspect_widget(widget._handle, (fallback ? fallback._handle : 0), _err); + _checkErr(wasm, _err); + _freeErr(wasm, _err); + return _takeCStr(wasm, _r); + }, + typedHandleParam(token, raw) { + const _err = _allocErr(wasm); + const _r = wasm.weaveffi_edge_cases_typed_handle_param(token, BigInt(raw), _err); + _checkErr(wasm, _err); + _freeErr(wasm, _err); + return _r; + }, + stringStream() { + const _err = _allocErr(wasm); + const _it = wasm.weaveffi_edge_cases_string_stream(_err); + _checkErr(wasm, _err); + _freeErr(wasm, _err); + return new _WeaveFFIIterator(wasm, _it, 4, + (it, slot, ep) => wasm.weaveffi_edge_cases_StringStreamIterator_next(it, slot, ep), + (it) => wasm.weaveffi_edge_cases_StringStreamIterator_destroy(it), + _checkErr, (w, p) => _takeCStr(w, new DataView(w.memory.buffer).getUint32(p, true))); + }, + scalarStream() { + const _err = _allocErr(wasm); + const _it = wasm.weaveffi_edge_cases_scalar_stream(_err); + _checkErr(wasm, _err); + _freeErr(wasm, _err); + return new _WeaveFFIIterator(wasm, _it, 4, + (it, slot, ep) => wasm.weaveffi_edge_cases_ScalarStreamIterator_next(it, slot, ep), + (it) => wasm.weaveffi_edge_cases_ScalarStreamIterator_destroy(it), + _checkErr, (w, p) => new DataView(w.memory.buffer).getInt32(p, true)); + }, + modeStream() { + const _err = _allocErr(wasm); + const _it = wasm.weaveffi_edge_cases_mode_stream(_err); + _checkErr(wasm, _err); + _freeErr(wasm, _err); + return new _WeaveFFIIterator(wasm, _it, 4, + (it, slot, ep) => wasm.weaveffi_edge_cases_ModeStreamIterator_next(it, slot, ep), + (it) => wasm.weaveffi_edge_cases_ModeStreamIterator_destroy(it), + _checkErr, (w, p) => new DataView(w.memory.buffer).getInt32(p, true)); + }, + asyncVoid() { + return new Promise((resolve, reject) => { + const ctxId = _nextCtxId++; + _asyncContexts.set(ctxId, { resolve, reject }); + wasm.weaveffi_edge_cases_async_void_async(_cbPtr_i32_i32, ctxId); + }); + }, + asyncBytes(size) { + return new Promise((resolve, reject) => { + const ctxId = _nextCtxId++; + _asyncContexts.set(ctxId, { resolve, reject, unwrap: (w, ptr, len) => { + const _b = ptr === 0 || len === 0 ? new Uint8Array(0) : new Uint8Array(w.memory.buffer, ptr, len).slice(); + if (ptr !== 0) w.weaveffi_free_bytes(ptr, len); + return _b; + } }); + wasm.weaveffi_edge_cases_async_bytes_async(size, _cbPtr_i32_i32_i32_i32, ctxId); + }); + }, + asyncStruct() { + return new Promise((resolve, reject) => { + const ctxId = _nextCtxId++; + _asyncContexts.set(ctxId, { resolve, reject, mkErr: _edgeErrorsErrorFrom, unwrap: (w, ptr, len) => { + const _b = ptr === 0 || len === 0 ? new Uint8Array(0) : new Uint8Array(w.memory.buffer, ptr, len).slice(); + if (ptr !== 0) w.weaveffi_free_bytes(ptr, len); + const _rd = new _BufReader(_b); + const _v = _read_edge_cases_Outer(_rd); + _rd.end(); + return _v; + } }); + wasm.weaveffi_edge_cases_async_struct_async(_cbPtr_i32_i32_i32_i32, ctxId); + }); + }, + asyncList() { + return new Promise((resolve, reject) => { + const ctxId = _nextCtxId++; + _asyncContexts.set(ctxId, { resolve, reject, mkErr: _edgeErrorsErrorFrom, unwrap: (w, ptr, len) => { + const _b = ptr === 0 || len === 0 ? new Uint8Array(0) : new Uint8Array(w.memory.buffer, ptr, len).slice(); + if (ptr !== 0) w.weaveffi_free_bytes(ptr, len); + const _rd = new _BufReader(_b); + const _v = (() => { const _n = _rd.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push(_read_edge_cases_Outer(_rd)); return _arr; })(); + _rd.end(); + return _v; + } }); + wasm.weaveffi_edge_cases_async_list_async(0, _cbPtr_i32_i32_i32_i32, ctxId); + }); + }, + asyncOptional() { + return new Promise((resolve, reject) => { + const ctxId = _nextCtxId++; + _asyncContexts.set(ctxId, { resolve, reject, unwrap: (w, ptr, len) => { + const _b = ptr === 0 || len === 0 ? new Uint8Array(0) : new Uint8Array(w.memory.buffer, ptr, len).slice(); + if (ptr !== 0) w.weaveffi_free_bytes(ptr, len); + const _rd = new _BufReader(_b); + const _v = (_rd.flag() ? _rd.i64() : null); + _rd.end(); + return _v; + } }); + wasm.weaveffi_edge_cases_async_optional_async(_cbPtr_i32_i32_i32_i32, ctxId); + }); + }, + asyncEnum() { + return new Promise((resolve, reject) => { + const ctxId = _nextCtxId++; + _asyncContexts.set(ctxId, { resolve, reject }); + wasm.weaveffi_edge_cases_async_enum_async(_cbPtr_i32_i32_i32, ctxId); + }); + }, + asyncKeywords(class_, import_) { + return new Promise((resolve, reject) => { + const ctxId = _nextCtxId++; + _asyncContexts.set(ctxId, { resolve, reject, unwrap: (w, p) => { + const _s = _readCStr(w, p); + if (p !== 0) w.weaveffi_free_string(p); + return _s; + } }); + const [a0_p, a0_s] = _cstr(wasm, class_); + wasm.weaveffi_edge_cases_async_keywords_async(a0_p, import_, _cbPtr_i32_i32_i32, ctxId); + wasm.weaveffi_dealloc(a0_p, a0_s); + }); + }, + keywordsRoundtrip(value) { + const a0_w = new _BufWriter(); + _write_edge_cases_Keywords(a0_w, value); + const [a0_p, a0_l] = _bytes(wasm, a0_w.finish()); + const _lp = wasm.weaveffi_alloc(4); + const _err = _allocErr(wasm); + const _r = wasm.weaveffi_edge_cases_keywords_roundtrip(a0_p, a0_l, _lp, _err); + wasm.weaveffi_dealloc(a0_p, a0_l); + _checkErr(wasm, _err); + _freeErr(wasm, _err); + const _len = new DataView(wasm.memory.buffer).getUint32(_lp, true); + wasm.weaveffi_dealloc(_lp, 4); + const _rd = new _BufReader(_takeBytes(wasm, _r, _len)); + const _out = _read_edge_cases_Keywords(_rd); + _rd.end(); + return _out; + }, + /** @deprecated Use Mode-based APIs */ + legacyValue(value) { + const _lp = wasm.weaveffi_alloc(4); + const _err = _allocErr(wasm); + const _r = wasm.weaveffi_edge_cases_legacy_value(value, _lp, _err); + _checkErr(wasm, _err); + _freeErr(wasm, _err); + const _len = new DataView(wasm.memory.buffer).getUint32(_lp, true); + wasm.weaveffi_dealloc(_lp, 4); + const _rd = new _BufReader(_takeBytes(wasm, _r, _len)); + const _out = _read_edge_cases_Retired(_rd); + _rd.end(); + return _out; + }, + registerBatchListener(callback) { + const _id = _nextLsnId++; + const _rid = wasm.weaveffi_edge_cases_register_batch_listener(_lsnPtr_weaveffi_edge_cases_OnBatch_fn, _id); + _listeners.set(_id, { callback, rid: _rid }); + return _id; + }, + unregisterBatchListener(id) { + const _l = _listeners.get(id); + if (_l === undefined) return; + _listeners.delete(id); + wasm.weaveffi_edge_cases_unregister_batch_listener(_l.rid); + }, + registerTickListener(callback) { + const _id = _nextLsnId++; + const _rid = wasm.weaveffi_edge_cases_register_tick_listener(_lsnPtr_weaveffi_edge_cases_OnTick_fn, _id); + _listeners.set(_id, { callback, rid: _rid }); + return _id; + }, + unregisterTickListener(id) { + const _l = _listeners.get(id); + if (_l === undefined) return; + _listeners.delete(id); + wasm.weaveffi_edge_cases_unregister_tick_listener(_l.rid); + }, + Widget: Widget, + Vault: Vault, + }, + }; +} + +// END weaveffi_wasm.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_events__README_md.snap b/crates/weaveffi-cli/tests/snapshots/wasm_events__README_md.snap deleted file mode 100644 index 2957a3c8..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_events__README_md.snap +++ /dev/null @@ -1,97 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI Wasm (experimental) - -This folder contains a minimal stub to help you load a `wasm32-unknown-unknown` build of your WeaveFFI library. - -Build (example): - -```bash -cargo build --target wasm32-unknown-unknown --release -``` - -Then serve the `.wasm` and use `weaveffi_wasm.js` to load it. - -## Complex Type Handling - -Wasm only supports numeric types natively (`i32`, `i64`, `f32`, `f64`). Complex types are encoded at the boundary as follows: - -### Records - -Records are **plain JavaScript objects**. A record crosses the boundary serialized in the WeaveFFI value-buffer format as one pointer + length pair in linear memory; the glue packs and unpacks it automatically, so there are no handles, no accessor functions, and nothing to free. - -### Enums - -C-style enums are passed as **`i32` values** corresponding to the variant's integer discriminant. Rich (algebraic) enums are plain objects tagged by variant name (`{ tag: "Circle", radius: 2 }`) and cross the boundary serialized in a value buffer, exactly like records. - -### Optionals - -Optional values map to **`null`** for the absent case. An optional crosses the boundary inside a value buffer as a one-byte presence flag followed by the value when present. The one exception is an optional interface, which stays a nullable object pointer (`0` = absent). - -### Lists and Maps - -Lists are JS arrays; maps are plain objects (a `Map` instance is also accepted on input). Both cross the boundary serialized in a value buffer as one pointer + length pair, recursing through element types. - -### Iterators - -`iter` functions return a **lazy JS iterator** (typed `IterableIterator`): each `next()` issues exactly one producer call, so iteration streams in constant memory. The producer handle is destroyed exactly once, on exhaustion or via `return()` when iteration stops early (a `for...of` loop calls `return()` automatically on `break` or `throw`). Abandoning an iterator without exhausting or closing it leaks the handle. - -### Callbacks and Listeners - -Each listener surfaces as a `register.../unregister...` pair. `register` takes a plain JS function and returns a numeric subscription id; `unregister` takes that id and stops delivery. Delivery is **synchronous and same-thread**: `wasm32-unknown-unknown` is single-threaded, so events fire only while a call into the module is on the stack (for example, a producer function that emits during its own execution). A producer that emits from a spawned thread cannot run on this target at all. - -Callback arguments are **borrowed for the duration of the callback**: strings, byte buffers, and buffered values (records, rich enums, optionals, lists, maps) are copied or decoded into JS values before your function runs, and interface arguments wrap producer-owned objects. Read what you need inside the callback and do not retain an interface wrapper or call `free()` on it. - -### Error Handling - -The generated JS wrappers automatically handle errors by passing an error -pointer as the last argument to each Wasm function. The error struct is -16 bytes on wasm32: `{ i32 code, char* message, uint8_t* payload_ptr, -size_t payload_len }`. Your Wasm module must export the following -functions: - -- `weaveffi_alloc(size: i32) -> i32`: allocate `size` bytes in linear memory -- `weaveffi_dealloc(ptr: i32, size: i32)`: release a `weaveffi_alloc` block -- `weaveffi_error_clear(err_ptr: i32)`: clear and free error resources -- `weaveffi_free_string(ptr: i32)`: free a producer-returned C string -- `weaveffi_free_bytes(ptr: i32, len: i32)`: free a producer-returned buffer - -Wrappers of functions declared `throws` raise the declaring module's typed -error class (a `WeaveFFIError` subclass with a per-code subclass, such as -`KeyNotFound`); every other wrapper raises the generic `WeaveFFIError` only -for producer panics and marshalling failures. When the matched error code -declares payload fields, the wrapper decodes them from the error's value -buffer and attaches them as properties on the thrown error. - -## API Reference - -### Module: `events` - -#### Functions - -##### `weaveffi_events_send_message` - -Send a message, triggering the OnMessage callback - -`weaveffi_events_send_message(text: i32) -> void` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `text` | `string` | `i32` | NUL-terminated C string pointer | - -##### `weaveffi_events_get_messages` - -Return an iterator over all sent messages - -`weaveffi_events_get_messages() -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `iter` | `i32` | opaque iterator handle | - - diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_events__package_json.snap b/crates/weaveffi-cli/tests/snapshots/wasm_events__package_json.snap deleted file mode 100644 index cee4ead6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_events__package_json.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -{ - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", - "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", - "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", - "type": "module", - "main": "weaveffi_wasm.js", - "types": "weaveffi_wasm.d.ts" -} diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_events__weaveffi_wasm_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_events__weaveffi_wasm_d_ts.snap deleted file mode 100644 index 6586aa26..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_events__weaveffi_wasm_d_ts.snap +++ /dev/null @@ -1,45 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// Generated TypeScript declarations for WeaveFFI Wasm bindings - -/** Base error for WeaveFFI failures: domain errors extend it, and it is - * thrown directly for unknown codes, marshalling failures, and producer - * panics. Carries the stable ABI `code`. */ -export declare class WeaveFFIError extends Error { - constructor(code: number, message?: string); - code: number; -} - -export interface WeaveffiWasmModule { - _raw: WebAssembly.Exports; - events: { - /** - * Send a message, triggering the OnMessage callback - * @throws {WeaveFFIError} if the native call fails - */ - sendMessage(text: string): void; - /** - * Return an iterator over all sent messages - * @returns A lazy iterator: one producer step per `next()` call. Exhaust it or call `return()` to release the producer handle (a `for...of` loop does both automatically); an abandoned iterator leaks the handle. - * @throws {WeaveFFIError} if the native call fails - */ - getMessages(): IterableIterator; - /** - * Register a listener for the `OnMessage` callback. - * - * @returns A subscription id for `unregisterMessageListener()`. - */ - registerMessageListener(callback: (message: string) => void): number; - /** Unregister a listener previously registered with `registerMessageListener()`. */ - unregisterMessageListener(id: number): void; - }; -} - -export function loadWeaveffiWasm(url: string): Promise; - -// END weaveffi_wasm.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_events__weaveffi_wasm_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_events__weaveffi_wasm_js.snap deleted file mode 100644 index f91dd413..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_events__weaveffi_wasm_js.snap +++ /dev/null @@ -1,244 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// WeaveFFI Wasm bindings (auto-generated) -// -// Boundary conventions for a wasm32-unknown-unknown build: -// -// Objects -> i32 pointer into linear memory (0 = null/absent) -// Enums -> i32 discriminant value -// i64/u64 -> JavaScript BigInt -// Strings -> NUL-terminated UTF-8 (const char*); a single i32 pointer -// Bytes -> i32 data pointer + i32 length (out_len for returns) -// Buffered -> records, rich enums, optionals, lists, and maps cross -// as one value buffer: i32 pointer + i32 length - -/** Base error for WeaveFFI failures: domain errors extend it, and it is - * thrown directly for unknown codes, marshalling failures, and producer - * panics. Carries the stable ABI `code`. */ -export class WeaveFFIError extends Error { - constructor(code, message) { - super(message ? `WeaveFFI error ${code}: ${message}` : `WeaveFFI error ${code}`); - this.name = new.target.name; - this.code = code; - } -} - -const _enc = new TextEncoder(); -const _dec = new TextDecoder(); - -// Stage a JS string as a NUL-terminated C string in linear memory. -// Returns [ptr, size] (size includes the NUL); release with _free. -function _cstr(wasm, str) { - const bytes = _enc.encode(str); - const size = bytes.length + 1; - const ptr = wasm.weaveffi_alloc(size); - const mem = new Uint8Array(wasm.memory.buffer, ptr, size); - mem.set(bytes); - mem[bytes.length] = 0; - return [ptr, size]; -} - -// Read a NUL-terminated C string (0 => null). Does not free. -function _readCStr(wasm, ptr) { - if (ptr === 0) return null; - const mem = new Uint8Array(wasm.memory.buffer); - let end = ptr; - while (mem[end] !== 0) end++; - return _dec.decode(mem.subarray(ptr, end)); -} - -// Read then free a producer-owned C string. -function _takeCStr(wasm, ptr) { - const s = _readCStr(wasm, ptr); - if (ptr !== 0) wasm.weaveffi_free_string(ptr); - return s; -} - -// Allocate a zeroed 16-byte error slot: -// { i32 code, char* message, uint8_t* payload_ptr, size_t payload_len }. -function _allocErr(wasm) { - const ptr = wasm.weaveffi_alloc(16); - new Uint8Array(wasm.memory.buffer, ptr, 16).fill(0); - return ptr; -} - -// Throw (and free the slot) if the error slot carries a non-zero code. -// Non-throwing wrappers route here: a non-zero code can only be a -// producer panic or a marshalling failure, surfaced as the generic -// WeaveFFIError. -function _checkErr(wasm, errPtr) { - const dv = new DataView(wasm.memory.buffer); - const code = dv.getInt32(errPtr, true); - if (code !== 0) { - const msgPtr = dv.getUint32(errPtr + 4, true); - const msg = _readCStr(wasm, msgPtr) || ''; - wasm.weaveffi_error_clear(errPtr); - wasm.weaveffi_dealloc(errPtr, 16); - throw new WeaveFFIError(code, msg); - } -} - -// Release an error slot on the success path. -function _freeErr(wasm, errPtr) { - wasm.weaveffi_dealloc(errPtr, 16); -} - -// Lazy wrapper over a producer iterator handle, implementing the JS -// iterator protocol: each next() issues exactly one producer next call -// and yields one converted element, so iteration streams in constant -// memory. The handle is destroyed exactly once: eagerly on exhaustion, -// on a next error, or from return() when iteration stops early (a -// for...of loop calls return() automatically on break or throw). -// Abandoning an iterator without exhausting or closing it leaks the -// producer handle: JS has no finalization hook that is reliable across -// every target this loader supports. -class _WeaveFFIIterator { - constructor(wasm, handle, slotSize, callNext, destroy, check, decode) { - this._wasm = wasm; - this._handle = handle; - this._slotSize = slotSize; - this._callNext = callNext; - this._destroyFn = destroy; - this._check = check; - this._decode = decode; - this._slot = wasm.weaveffi_alloc(slotSize); - } - // Destroy the handle and release the element slot exactly once. - _close() { - if (this._handle === 0) return; - this._destroyFn(this._handle); - this._handle = 0; - this._wasm.weaveffi_dealloc(this._slot, this._slotSize); - this._slot = 0; - } - next() { - if (this._handle === 0) return { done: true, value: undefined }; - const wasm = this._wasm; - const _err = _allocErr(wasm); - let _has; - try { - _has = this._callNext(this._handle, this._slot, _err); - // Throws (and releases the slot) on a non-zero code. - this._check(wasm, _err); - } catch (e) { - this._close(); - throw e; - } - _freeErr(wasm, _err); - if (_has === 0) { - this._close(); - return { done: true, value: undefined }; - } - return { done: false, value: this._decode(wasm, this._slot) }; - } - // Early-exit cleanup; for...of calls this on break/throw. - return(value) { - this._close(); - return { done: true, value }; - } - [Symbol.iterator]() { - return this; - } -} - -function _registerTrampoline(table, paramTypes, handler) { - const idx = table.grow(1); - table.set(idx, new WebAssembly.Function( - { parameters: paramTypes, results: [] }, - handler - )); - return idx; -} - -/** - * Load a WeaveFFI Wasm module from the given URL. - * - * @param {string} url - URL to the `.wasm` file. - * @returns {Promise} The API bindings. - * - * Exported functions follow the C ABI naming convention: - * weaveffi_{module}_{function}(params...) -> result - * - * @example - * const api = await loadWeaveffiWasm('lib.wasm'); - * - * // Primitive: plain numbers in, number out. - * const sum = api.math.add(1, 2); - * - * // Record: plain objects in and out (serialized automatically). - * const person = api.contacts.create({ name: 'Ada', age: 36 }); - * - * // Enum: pass the integer discriminant. - * api.ui.set_color(0); // 0 = first variant - * - * // Optional: pass null to omit, a value to provide. - * api.config.set_timeout(5000); // present - * api.config.set_timeout(null); // absent - * - * // List/Map: pass arrays/objects; receive arrays/objects. - * const names = api.data.all_names(); - */ -export async function loadWeaveffiWasm(url) { - const response = await fetch(url); - const bytes = await response.arrayBuffer(); - const { instance } = await WebAssembly.instantiate(bytes, {}); - const wasm = instance.exports; - - const _table = wasm.__indirect_function_table; - - // Listener subscriptions, keyed by the context id the loader - // threads through the C ABI's void* context slot. Each entry - // holds the JS callback and the producer's subscription id. - let _nextLsnId = 1; - const _listeners = new Map(); - - const _lsnPtr_weaveffi_events_OnMessage_fn = _registerTrampoline(_table, ['i32', 'i32'], (a0, _ctx) => { - const _l = _listeners.get(_ctx); - if (_l === undefined) return; - const _p0 = _readCStr(wasm, a0); - _l.callback(_p0); - }); - - return { - _raw: wasm, - events: { - sendMessage(text) { - const [a0_p, a0_s] = _cstr(wasm, text); - const _err = _allocErr(wasm); - wasm.weaveffi_events_send_message(a0_p, _err); - wasm.weaveffi_dealloc(a0_p, a0_s); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - }, - getMessages() { - const _err = _allocErr(wasm); - const _it = wasm.weaveffi_events_get_messages(_err); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return new _WeaveFFIIterator(wasm, _it, 4, - (it, slot, ep) => wasm.weaveffi_events_GetMessagesIterator_next(it, slot, ep), - (it) => wasm.weaveffi_events_GetMessagesIterator_destroy(it), - _checkErr, (w, p) => _takeCStr(w, new DataView(w.memory.buffer).getUint32(p, true))); - }, - registerMessageListener(callback) { - const _id = _nextLsnId++; - const _rid = wasm.weaveffi_events_register_message_listener(_lsnPtr_weaveffi_events_OnMessage_fn, _id); - _listeners.set(_id, { callback, rid: _rid }); - return _id; - }, - unregisterMessageListener(id) { - const _l = _listeners.get(id); - if (_l === undefined) return; - _listeners.delete(id); - wasm.weaveffi_events_unregister_message_listener(_l.rid); - }, - }, - }; -} - -// END weaveffi_wasm.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_inventory__README_md.snap b/crates/weaveffi-cli/tests/snapshots/wasm_inventory__README_md.snap deleted file mode 100644 index 5ce8c86c..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_inventory__README_md.snap +++ /dev/null @@ -1,247 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI Wasm (experimental) - -This folder contains a minimal stub to help you load a `wasm32-unknown-unknown` build of your WeaveFFI library. - -Build (example): - -```bash -cargo build --target wasm32-unknown-unknown --release -``` - -Then serve the `.wasm` and use `weaveffi_wasm.js` to load it. - -## Complex Type Handling - -Wasm only supports numeric types natively (`i32`, `i64`, `f32`, `f64`). Complex types are encoded at the boundary as follows: - -### Records - -Records are **plain JavaScript objects**. A record crosses the boundary serialized in the WeaveFFI value-buffer format as one pointer + length pair in linear memory; the glue packs and unpacks it automatically, so there are no handles, no accessor functions, and nothing to free. - -### Enums - -C-style enums are passed as **`i32` values** corresponding to the variant's integer discriminant. Rich (algebraic) enums are plain objects tagged by variant name (`{ tag: "Circle", radius: 2 }`) and cross the boundary serialized in a value buffer, exactly like records. - -### Optionals - -Optional values map to **`null`** for the absent case. An optional crosses the boundary inside a value buffer as a one-byte presence flag followed by the value when present. The one exception is an optional interface, which stays a nullable object pointer (`0` = absent). - -### Lists and Maps - -Lists are JS arrays; maps are plain objects (a `Map` instance is also accepted on input). Both cross the boundary serialized in a value buffer as one pointer + length pair, recursing through element types. - -### Iterators - -`iter` functions return a **lazy JS iterator** (typed `IterableIterator`): each `next()` issues exactly one producer call, so iteration streams in constant memory. The producer handle is destroyed exactly once, on exhaustion or via `return()` when iteration stops early (a `for...of` loop calls `return()` automatically on `break` or `throw`). Abandoning an iterator without exhausting or closing it leaks the handle. - -### Error Handling - -The generated JS wrappers automatically handle errors by passing an error -pointer as the last argument to each Wasm function. The error struct is -16 bytes on wasm32: `{ i32 code, char* message, uint8_t* payload_ptr, -size_t payload_len }`. Your Wasm module must export the following -functions: - -- `weaveffi_alloc(size: i32) -> i32`: allocate `size` bytes in linear memory -- `weaveffi_dealloc(ptr: i32, size: i32)`: release a `weaveffi_alloc` block -- `weaveffi_error_clear(err_ptr: i32)`: clear and free error resources -- `weaveffi_free_string(ptr: i32)`: free a producer-returned C string -- `weaveffi_free_bytes(ptr: i32, len: i32)`: free a producer-returned buffer - -Wrappers of functions declared `throws` raise the declaring module's typed -error class (a `WeaveFFIError` subclass with a per-code subclass, such as -`KeyNotFound`); every other wrapper raises the generic `WeaveFFIError` only -for producer panics and marshalling failures. When the matched error code -declares payload fields, the wrapper decodes them from the error's value -buffer and attaches them as properties on the thrown error. - -## API Reference - -### Module: `products` - -#### Error Domain: `ProductsError` - -Throwing wrappers in this module raise `ProductsError` (a `WeaveFFIError` subclass); each code below is its own subclass carrying the stable `code`. Payload fields are decoded from the error's value buffer and attached as properties on the thrown error. - -| Class | Code | Default Message | Payload Fields | -|-------|------|-----------------|----------------| -| `InvalidPrice` | `1` | price must be positive | (none) | -| `ProductNotFound` | `2` | product not found | (none) | - -#### Interfaces - -##### `Catalog` - -A product catalog owning its products - -Passed as an **opaque object pointer** (`i32`), wrapped by a JS class. Constructors return an owned handle; methods pass the handle as the implicit leading `self` argument; `free()` releases the handle via the destroy symbol. - -##### `weaveffi_products_Catalog_new` - -`weaveffi_products_Catalog_new() -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `Catalog` | `i32` | opaque object pointer | - -##### `weaveffi_products_Catalog_add_product` - -`weaveffi_products_Catalog_add_product(name: i32, price: f64, category: i32) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `name` | `string` | `i32` | NUL-terminated C string pointer | -| `price` | `f64` | `f64` | native Wasm f64 | -| `category` | `Category` | `i32` | variant discriminant | -| _returns_ | `Product` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_products_Catalog_get_product` - -`weaveffi_products_Catalog_get_product(id: i64) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `id` | `i64` | `i64` | native Wasm i64 | -| _returns_ | `Product` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_products_Catalog_search` - -`weaveffi_products_Catalog_search(category: i32) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `category` | `Category` | `i32` | variant discriminant | -| _returns_ | `[Product]` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_products_Catalog_update_price` - -`weaveffi_products_Catalog_update_price(id: i64, price: f64) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `id` | `i64` | `i64` | native Wasm i64 | -| `price` | `f64` | `f64` | native Wasm f64 | -| _returns_ | `bool` | `i32` | 0 = false, 1 = true | - -##### `weaveffi_products_Catalog_remove` - -`weaveffi_products_Catalog_remove(id: i64) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `id` | `i64` | `i64` | native Wasm i64 | -| _returns_ | `bool` | `i32` | 0 = false, 1 = true | - -##### `weaveffi_products_Catalog_destroy` - -Releases the object reference. Called by the wrapper's `free()`. - -#### Structs - -##### `Product` - -A plain JS object, serialized in a value buffer (`i32` ptr + `i32` len) at the boundary. Fields in declaration (wire) order: - -| Field | Type | -|-------|------| -| `id` | `i64` | -| `name` | `string` | -| `description` | `string?` | -| `price` | `f64` | -| `category` | `Category` | -| `tags` | `[string]` | - -#### Enums - -##### `Category` - -Passed as `i32` discriminant. - -| Variant | Value | -|---------|-------| -| `Electronics` | `0` | -| `Clothing` | `1` | -| `Food` | `2` | -| `Books` | `3` | - -### Module: `orders` - -#### Error Domain: `OrdersError` - -Throwing wrappers in this module raise `OrdersError` (a `WeaveFFIError` subclass); each code below is its own subclass carrying the stable `code`. Payload fields are decoded from the error's value buffer and attached as properties on the thrown error. - -| Class | Code | Default Message | Payload Fields | -|-------|------|-----------------|----------------| -| `OrderNotFound` | `1` | order not found | (none) | -| `EmptyOrder` | `2` | order must contain at least one item | (none) | - -#### Functions - -##### `weaveffi_orders_create_order` - -`weaveffi_orders_create_order(items: i32, i32) -> i64` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `items` | `[OrderItem]` | `i32, i32` | value buffer: ptr + len in linear memory | -| _returns_ | `i64` | `i64` | native Wasm i64 | - -##### `weaveffi_orders_get_order` - -`weaveffi_orders_get_order(id: i64) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `id` | `i64` | `i64` | native Wasm i64 | -| _returns_ | `Order` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_orders_cancel_order` - -`weaveffi_orders_cancel_order(id: i64) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `id` | `i64` | `i64` | native Wasm i64 | -| _returns_ | `bool` | `i32` | 0 = false, 1 = true | - -##### `weaveffi_orders_add_product_to_order` - -`weaveffi_orders_add_product_to_order(order_id: i64, product: i32, i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `order_id` | `i64` | `i64` | native Wasm i64 | -| `product` | `Product` | `i32, i32` | value buffer: ptr + len in linear memory | -| _returns_ | `bool` | `i32` | 0 = false, 1 = true | - -#### Structs - -##### `OrderItem` - -A plain JS object, serialized in a value buffer (`i32` ptr + `i32` len) at the boundary. Fields in declaration (wire) order: - -| Field | Type | -|-------|------| -| `product_id` | `i64` | -| `quantity` | `i32` | -| `unit_price` | `f64` | - -##### `Order` - -A plain JS object, serialized in a value buffer (`i32` ptr + `i32` len) at the boundary. Fields in declaration (wire) order: - -| Field | Type | -|-------|------| -| `id` | `i64` | -| `items` | `[OrderItem]` | -| `total` | `f64` | -| `status` | `string` | - - diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_inventory__package_json.snap b/crates/weaveffi-cli/tests/snapshots/wasm_inventory__package_json.snap deleted file mode 100644 index cee4ead6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_inventory__package_json.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -{ - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", - "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", - "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", - "type": "module", - "main": "weaveffi_wasm.js", - "types": "weaveffi_wasm.d.ts" -} diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_inventory__weaveffi_wasm_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_inventory__weaveffi_wasm_d_ts.snap deleted file mode 100644 index 9cc08191..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_inventory__weaveffi_wasm_d_ts.snap +++ /dev/null @@ -1,140 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// Generated TypeScript declarations for WeaveFFI Wasm bindings - -/** Base error for WeaveFFI failures: domain errors extend it, and it is - * thrown directly for unknown codes, marshalling failures, and producer - * panics. Carries the stable ABI `code`. */ -export declare class WeaveFFIError extends Error { - constructor(code: number, message?: string); - code: number; -} - -/** Base error for the `products` module's error domain. */ -export declare class ProductsError extends WeaveFFIError { - static readonly InvalidPrice: typeof InvalidPrice; - static readonly ProductNotFound: typeof ProductNotFound; -} - -/** price must be positive */ -export declare class InvalidPrice extends ProductsError { - constructor(message?: string); - static readonly CODE: 1; -} - -/** product not found */ -export declare class ProductNotFound extends ProductsError { - constructor(message?: string); - static readonly CODE: 2; -} - -/** Base error for the `orders` module's error domain. */ -export declare class OrdersError extends WeaveFFIError { - static readonly OrderNotFound: typeof OrderNotFound; - static readonly EmptyOrder: typeof EmptyOrder; -} - -/** order not found */ -export declare class OrderNotFound extends OrdersError { - constructor(message?: string); - static readonly CODE: 1; -} - -/** order must contain at least one item */ -export declare class EmptyOrder extends OrdersError { - constructor(message?: string); - static readonly CODE: 2; -} - -export interface Product { - id: bigint; - name: string; - description: string | null; - price: number; - category: Category; - tags: string[]; -} - -export declare const Category: Readonly<{ - Electronics: 0; - Clothing: 1; - Food: 2; - Books: 3; -}>; - -/** A product catalog owning its products */ -export declare class Catalog { - /** - * @throws {WeaveFFIError} if the native call fails - */ - constructor(); - /** - * @throws {ProductsError} on a domain error code - */ - addProduct(name: string, price: number, category: Category): Product; - /** - * @throws {ProductsError} on a domain error code - */ - getProduct(id: bigint): Product; - /** - * @throws {WeaveFFIError} if the native call fails - */ - search(category: Category): Product[]; - /** - * @throws {ProductsError} on a domain error code - */ - updatePrice(id: bigint, price: number): boolean; - /** - * @throws {WeaveFFIError} if the native call fails - */ - remove(id: bigint): boolean; - /** Releases the producer-owned handle exactly once. */ - free(): void; -} - -export interface OrderItem { - product_id: bigint; - quantity: number; - unit_price: number; -} - -export interface Order { - id: bigint; - items: OrderItem[]; - total: number; - status: string; -} - -export interface WeaveffiWasmModule { - _raw: WebAssembly.Exports; - products: { - Catalog: typeof Catalog; - }; - orders: { - /** - * @throws {OrdersError} on a domain error code - */ - createOrder(items: OrderItem[]): bigint; - /** - * @throws {OrdersError} on a domain error code - */ - getOrder(id: bigint): Order; - /** - * @throws {WeaveFFIError} if the native call fails - */ - cancelOrder(id: bigint): boolean; - /** - * @throws {WeaveFFIError} if the native call fails - */ - addProductToOrder(orderId: bigint, product: Product): boolean; - }; -} - -export function loadWeaveffiWasm(url: string): Promise; - -// END weaveffi_wasm.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_inventory__weaveffi_wasm_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_inventory__weaveffi_wasm_js.snap deleted file mode 100644 index 39f6cdc7..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_inventory__weaveffi_wasm_js.snap +++ /dev/null @@ -1,561 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// WeaveFFI Wasm bindings (auto-generated) -// -// Boundary conventions for a wasm32-unknown-unknown build: -// -// Objects -> i32 pointer into linear memory (0 = null/absent) -// Enums -> i32 discriminant value -// i64/u64 -> JavaScript BigInt -// Strings -> NUL-terminated UTF-8 (const char*); a single i32 pointer -// Bytes -> i32 data pointer + i32 length (out_len for returns) -// Buffered -> records, rich enums, optionals, lists, and maps cross -// as one value buffer: i32 pointer + i32 length - -/** Base error for WeaveFFI failures: domain errors extend it, and it is - * thrown directly for unknown codes, marshalling failures, and producer - * panics. Carries the stable ABI `code`. */ -export class WeaveFFIError extends Error { - constructor(code, message) { - super(message ? `WeaveFFI error ${code}: ${message}` : `WeaveFFI error ${code}`); - this.name = new.target.name; - this.code = code; - } -} - -/** Base error for the `products` module's error domain. */ -export class ProductsError extends WeaveFFIError {} - -// price must be positive -export class InvalidPrice extends ProductsError { - constructor(message = "price must be positive") { - super(1, message); - } -} -InvalidPrice.CODE = 1; -ProductsError.InvalidPrice = InvalidPrice; - -// product not found -export class ProductNotFound extends ProductsError { - constructor(message = "product not found") { - super(2, message); - } -} -ProductNotFound.CODE = 2; -ProductsError.ProductNotFound = ProductNotFound; - -const _PRODUCTS_ERROR_CODES = Object.freeze({ - 1: InvalidPrice, - 2: ProductNotFound, -}); - -// Build the ProductsError subclass matching `code`, or a generic -// WeaveFFIError for codes outside the domain (panics, marshalling). -function _productsErrorFrom(wasm, code, message, payloadPtr, payloadLen) { - const _cls = _PRODUCTS_ERROR_CODES[code]; - const _e = _cls ? (message ? new _cls(message) : new _cls()) : new WeaveFFIError(code, message); - return _e; -} - -/** Base error for the `orders` module's error domain. */ -export class OrdersError extends WeaveFFIError {} - -// order not found -export class OrderNotFound extends OrdersError { - constructor(message = "order not found") { - super(1, message); - } -} -OrderNotFound.CODE = 1; -OrdersError.OrderNotFound = OrderNotFound; - -// order must contain at least one item -export class EmptyOrder extends OrdersError { - constructor(message = "order must contain at least one item") { - super(2, message); - } -} -EmptyOrder.CODE = 2; -OrdersError.EmptyOrder = EmptyOrder; - -const _ORDERS_ERROR_CODES = Object.freeze({ - 1: OrderNotFound, - 2: EmptyOrder, -}); - -// Build the OrdersError subclass matching `code`, or a generic -// WeaveFFIError for codes outside the domain (panics, marshalling). -function _ordersErrorFrom(wasm, code, message, payloadPtr, payloadLen) { - const _cls = _ORDERS_ERROR_CODES[code]; - const _e = _cls ? (message ? new _cls(message) : new _cls()) : new WeaveFFIError(code, message); - return _e; -} - -const _enc = new TextEncoder(); -const _dec = new TextDecoder(); - -// Stage a JS string as a NUL-terminated C string in linear memory. -// Returns [ptr, size] (size includes the NUL); release with _free. -function _cstr(wasm, str) { - const bytes = _enc.encode(str); - const size = bytes.length + 1; - const ptr = wasm.weaveffi_alloc(size); - const mem = new Uint8Array(wasm.memory.buffer, ptr, size); - mem.set(bytes); - mem[bytes.length] = 0; - return [ptr, size]; -} - -// Read a NUL-terminated C string (0 => null). Does not free. -function _readCStr(wasm, ptr) { - if (ptr === 0) return null; - const mem = new Uint8Array(wasm.memory.buffer); - let end = ptr; - while (mem[end] !== 0) end++; - return _dec.decode(mem.subarray(ptr, end)); -} - -// Read then free a producer-owned C string. -function _takeCStr(wasm, ptr) { - const s = _readCStr(wasm, ptr); - if (ptr !== 0) wasm.weaveffi_free_string(ptr); - return s; -} - -// Stage a byte buffer (or an encoded value buffer); returns [ptr, len]; -// release with weaveffi_dealloc(ptr, len). -function _bytes(wasm, data) { - const u8 = data instanceof Uint8Array ? data : new Uint8Array(data); - const ptr = wasm.weaveffi_alloc(u8.length); - if (u8.length) new Uint8Array(wasm.memory.buffer, ptr, u8.length).set(u8); - return [ptr, u8.length]; -} - -// Copy then free a producer-owned byte (or value) buffer. -function _takeBytes(wasm, ptr, len) { - if (ptr === 0 || len === 0) return new Uint8Array(0); - const copy = new Uint8Array(wasm.memory.buffer, ptr, len).slice(); - wasm.weaveffi_free_bytes(ptr, len); - return copy; -} - -// Growable little-endian byte writer implementing the WeaveFFI value-buffer -// wire format: values are packed back to back with no alignment; lengths and -// counts are u32; strings are u32 byte length + UTF-8 bytes (no NUL). -class _BufWriter { - constructor() { - this._u8 = new Uint8Array(64); - this._dv = new DataView(this._u8.buffer); - this._len = 0; - } - _need(n) { - if (this._len + n <= this._u8.length) return; - let cap = this._u8.length * 2; - while (cap < this._len + n) cap *= 2; - const u8 = new Uint8Array(cap); - u8.set(this._u8.subarray(0, this._len)); - this._u8 = u8; - this._dv = new DataView(u8.buffer); - } - bool(v) { this._need(1); this._dv.setUint8(this._len, v ? 1 : 0); this._len += 1; } - i8(v) { this._need(1); this._dv.setInt8(this._len, v); this._len += 1; } - u8(v) { this._need(1); this._dv.setUint8(this._len, v); this._len += 1; } - i16(v) { this._need(2); this._dv.setInt16(this._len, v, true); this._len += 2; } - u16(v) { this._need(2); this._dv.setUint16(this._len, v, true); this._len += 2; } - i32(v) { this._need(4); this._dv.setInt32(this._len, v, true); this._len += 4; } - u32(v) { this._need(4); this._dv.setUint32(this._len, v, true); this._len += 4; } - i64(v) { this._need(8); this._dv.setBigInt64(this._len, BigInt(v), true); this._len += 8; } - u64(v) { this._need(8); this._dv.setBigUint64(this._len, BigInt(v), true); this._len += 8; } - f32(v) { this._need(4); this._dv.setFloat32(this._len, v, true); this._len += 4; } - f64(v) { this._need(8); this._dv.setFloat64(this._len, v, true); this._len += 8; } - len(n) { this.u32(n); } - flag(present) { this.u8(present ? 1 : 0); } - str(v) { - const b = _enc.encode(v); - this.len(b.length); - this._need(b.length); - this._u8.set(b, this._len); - this._len += b.length; - } - bytes(v) { - const b = v instanceof Uint8Array ? v : new Uint8Array(v); - this.len(b.length); - this._need(b.length); - this._u8.set(b, this._len); - this._len += b.length; - } - finish() { return this._u8.subarray(0, this._len); } -} - -const _bufDec = new TextDecoder('utf-8', { fatal: true }); - -// Strict little-endian reader for the WeaveFFI value-buffer wire format. A -// malformed buffer (truncation, invalid bool or flag bytes, an oversized -// length prefix, invalid UTF-8, trailing bytes) is a producer/consumer -// contract violation and throws the generic brand error; code -3 marks a -// consumer-side marshalling failure. -class _BufReader { - constructor(bytes) { - this._u8 = bytes; - this._dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); - this._pos = 0; - } - _bad(what) { - throw new WeaveFFIError(-3, 'malformed value buffer: ' + what); - } - _take(n, what) { - if (this._pos + n > this._u8.length) this._bad('truncated ' + what); - const at = this._pos; - this._pos += n; - return at; - } - bool() { - const b = this._u8[this._take(1, 'bool')]; - if (b > 1) this._bad('bool byte out of range'); - return b === 1; - } - i8() { return this._dv.getInt8(this._take(1, 'i8')); } - u8() { return this._u8[this._take(1, 'u8')]; } - i16() { return this._dv.getInt16(this._take(2, 'i16'), true); } - u16() { return this._dv.getUint16(this._take(2, 'u16'), true); } - i32() { return this._dv.getInt32(this._take(4, 'i32'), true); } - u32() { return this._dv.getUint32(this._take(4, 'u32'), true); } - i64() { return this._dv.getBigInt64(this._take(8, 'i64'), true); } - u64() { return this._dv.getBigUint64(this._take(8, 'u64'), true); } - f32() { return this._dv.getFloat32(this._take(4, 'f32'), true); } - f64() { return this._dv.getFloat64(this._take(8, 'f64'), true); } - len() { - const n = this.u32(); - if (n > this._u8.length - this._pos) this._bad('length prefix exceeds remaining buffer'); - return n; - } - flag() { - const b = this._u8[this._take(1, 'option flag')]; - if (b > 1) this._bad('option flag byte out of range'); - return b === 1; - } - str() { - const n = this.len(); - const at = this._take(n, 'string bytes'); - try { - return _bufDec.decode(this._u8.subarray(at, at + n)); - } catch (e) { - this._bad('string is not valid UTF-8'); - } - } - bytes() { - const n = this.len(); - const at = this._take(n, 'byte buffer'); - return this._u8.subarray(at, at + n).slice(); - } - end() { - if (this._pos !== this._u8.length) this._bad('trailing bytes after value'); - } -} - -// Allocate a zeroed 16-byte error slot: -// { i32 code, char* message, uint8_t* payload_ptr, size_t payload_len }. -function _allocErr(wasm) { - const ptr = wasm.weaveffi_alloc(16); - new Uint8Array(wasm.memory.buffer, ptr, 16).fill(0); - return ptr; -} - -// Throw (and free the slot) if the error slot carries a non-zero code. -// Non-throwing wrappers route here: a non-zero code can only be a -// producer panic or a marshalling failure, surfaced as the generic -// WeaveFFIError. -function _checkErr(wasm, errPtr) { - const dv = new DataView(wasm.memory.buffer); - const code = dv.getInt32(errPtr, true); - if (code !== 0) { - const msgPtr = dv.getUint32(errPtr + 4, true); - const msg = _readCStr(wasm, msgPtr) || ''; - wasm.weaveffi_error_clear(errPtr); - wasm.weaveffi_dealloc(errPtr, 16); - throw new WeaveFFIError(code, msg); - } -} - -// Release an error slot on the success path. -function _freeErr(wasm, errPtr) { - wasm.weaveffi_dealloc(errPtr, 16); -} - -// Throw the `ProductsError` domain error (and free the slot) if the error slot -// carries a non-zero code. -function _checkProductsError(wasm, errPtr) { - const dv = new DataView(wasm.memory.buffer); - const code = dv.getInt32(errPtr, true); - if (code !== 0) { - const msg = _readCStr(wasm, dv.getUint32(errPtr + 4, true)) || ''; - const _e = _productsErrorFrom(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)); - wasm.weaveffi_error_clear(errPtr); - wasm.weaveffi_dealloc(errPtr, 16); - throw _e; - } -} - -// Throw the `OrdersError` domain error (and free the slot) if the error slot -// carries a non-zero code. -function _checkOrdersError(wasm, errPtr) { - const dv = new DataView(wasm.memory.buffer); - const code = dv.getInt32(errPtr, true); - if (code !== 0) { - const msg = _readCStr(wasm, dv.getUint32(errPtr + 4, true)) || ''; - const _e = _ordersErrorFrom(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)); - wasm.weaveffi_error_clear(errPtr); - wasm.weaveffi_dealloc(errPtr, 16); - throw _e; - } -} - -// Serialize a `products.Product` record into the value-buffer wire format. -function _write_products_Product(w, v) { - w.i64(v.id); - w.str(v.name); - if (v.description === null || v.description === undefined) { - w.flag(false); - } else { - w.flag(true); - w.str(v.description); - } - w.f64(v.price); - w.i32(v.category); - const _a1 = v.tags || []; - w.len(_a1.length); - for (const _e1 of _a1) { - w.str(_e1); - } -} - -// Decode a `products.Product` record from the value-buffer wire format. -function _read_products_Product(r) { - const v = {}; - v.id = r.i64(); - v.name = r.str(); - v.description = (r.flag() ? r.str() : null); - v.price = r.f64(); - v.category = r.i32(); - v.tags = (() => { const _n = r.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push(r.str()); return _arr; })(); - return v; -} - -// Serialize a `orders.OrderItem` record into the value-buffer wire format. -function _write_orders_OrderItem(w, v) { - w.i64(v.product_id); - w.i32(v.quantity); - w.f64(v.unit_price); -} - -// Decode a `orders.OrderItem` record from the value-buffer wire format. -function _read_orders_OrderItem(r) { - const v = {}; - v.product_id = r.i64(); - v.quantity = r.i32(); - v.unit_price = r.f64(); - return v; -} - -// Serialize a `orders.Order` record into the value-buffer wire format. -function _write_orders_Order(w, v) { - w.i64(v.id); - const _a1 = v.items || []; - w.len(_a1.length); - for (const _e1 of _a1) { - _write_orders_OrderItem(w, _e1); - } - w.f64(v.total); - w.str(v.status); -} - -// Decode a `orders.Order` record from the value-buffer wire format. -function _read_orders_Order(r) { - const v = {}; - v.id = r.i64(); - v.items = (() => { const _n = r.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push(_read_orders_OrderItem(r)); return _arr; })(); - v.total = r.f64(); - v.status = r.str(); - return v; -} - -export const Category = Object.freeze({ - Electronics: 0, - Clothing: 1, - Food: 2, - Books: 3, -}); - -/** - * Load a WeaveFFI Wasm module from the given URL. - * - * @param {string} url - URL to the `.wasm` file. - * @returns {Promise} The API bindings. - * - * Exported functions follow the C ABI naming convention: - * weaveffi_{module}_{function}(params...) -> result - * - * @example - * const api = await loadWeaveffiWasm('lib.wasm'); - * - * // Primitive: plain numbers in, number out. - * const sum = api.math.add(1, 2); - * - * // Record: plain objects in and out (serialized automatically). - * const person = api.contacts.create({ name: 'Ada', age: 36 }); - * - * // Enum: pass the integer discriminant. - * api.ui.set_color(0); // 0 = first variant - * - * // Optional: pass null to omit, a value to provide. - * api.config.set_timeout(5000); // present - * api.config.set_timeout(null); // absent - * - * // List/Map: pass arrays/objects; receive arrays/objects. - * const names = api.data.all_names(); - */ -export async function loadWeaveffiWasm(url) { - const response = await fetch(url); - const bytes = await response.arrayBuffer(); - const { instance } = await WebAssembly.instantiate(bytes, {}); - const wasm = instance.exports; - - // A product catalog owning its products - class Catalog { - constructor() { - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_products_Catalog_new(_err); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - this._handle = _r; - } - static _wrap(handle) { - const _o = Object.create(Catalog.prototype); - _o._handle = handle; - return _o; - } - free() { - if (this._handle !== 0) { - wasm.weaveffi_products_Catalog_destroy(this._handle); - this._handle = 0; - } - } - addProduct(name, price, category) { - const [a0_p, a0_s] = _cstr(wasm, name); - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_products_Catalog_add_product(this._handle, a0_p, price, category, _lp, _err); - wasm.weaveffi_dealloc(a0_p, a0_s); - _checkProductsError(wasm, _err); - _freeErr(wasm, _err); - const _len = new DataView(wasm.memory.buffer).getUint32(_lp, true); - wasm.weaveffi_dealloc(_lp, 4); - const _rd = new _BufReader(_takeBytes(wasm, _r, _len)); - const _out = _read_products_Product(_rd); - _rd.end(); - return _out; - } - getProduct(id) { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_products_Catalog_get_product(this._handle, BigInt(id), _lp, _err); - _checkProductsError(wasm, _err); - _freeErr(wasm, _err); - const _len = new DataView(wasm.memory.buffer).getUint32(_lp, true); - wasm.weaveffi_dealloc(_lp, 4); - const _rd = new _BufReader(_takeBytes(wasm, _r, _len)); - const _out = _read_products_Product(_rd); - _rd.end(); - return _out; - } - search(category) { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_products_Catalog_search(this._handle, category, _lp, _err); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - const _len = new DataView(wasm.memory.buffer).getUint32(_lp, true); - wasm.weaveffi_dealloc(_lp, 4); - const _rd = new _BufReader(_takeBytes(wasm, _r, _len)); - const _out = (() => { const _n = _rd.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push(_read_products_Product(_rd)); return _arr; })(); - _rd.end(); - return _out; - } - updatePrice(id, price) { - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_products_Catalog_update_price(this._handle, BigInt(id), price, _err); - _checkProductsError(wasm, _err); - _freeErr(wasm, _err); - return _r !== 0; - } - remove(id) { - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_products_Catalog_remove(this._handle, BigInt(id), _err); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r !== 0; - } - } - - return { - _raw: wasm, - products: { - Catalog: Catalog, - }, - orders: { - createOrder(items) { - const a0_w = new _BufWriter(); - const _a1 = items || []; - a0_w.len(_a1.length); - for (const _e1 of _a1) { - _write_orders_OrderItem(a0_w, _e1); - } - const [a0_p, a0_l] = _bytes(wasm, a0_w.finish()); - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_orders_create_order(a0_p, a0_l, _err); - wasm.weaveffi_dealloc(a0_p, a0_l); - _checkOrdersError(wasm, _err); - _freeErr(wasm, _err); - return _r; - }, - getOrder(id) { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_orders_get_order(BigInt(id), _lp, _err); - _checkOrdersError(wasm, _err); - _freeErr(wasm, _err); - const _len = new DataView(wasm.memory.buffer).getUint32(_lp, true); - wasm.weaveffi_dealloc(_lp, 4); - const _rd = new _BufReader(_takeBytes(wasm, _r, _len)); - const _out = _read_orders_Order(_rd); - _rd.end(); - return _out; - }, - cancelOrder(id) { - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_orders_cancel_order(BigInt(id), _err); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r !== 0; - }, - addProductToOrder(orderId, product) { - const a1_w = new _BufWriter(); - _write_products_Product(a1_w, product); - const [a1_p, a1_l] = _bytes(wasm, a1_w.finish()); - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_orders_add_product_to_order(BigInt(orderId), a1_p, a1_l, _err); - wasm.weaveffi_dealloc(a1_p, a1_l); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r !== 0; - }, - }, - }; -} - -// END weaveffi_wasm.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 index 64b30835..449719f4 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__README_md.snap @@ -55,6 +55,7 @@ pointer as the last argument to each Wasm function. The error struct is size_t payload_len }`. Your Wasm module must export the following functions: +- `weaveffi_abi_version() -> i32`: the ABI revision the module implements; the loader refuses a mismatch - `weaveffi_alloc(size: i32) -> i32`: allocate `size` bytes in linear memory - `weaveffi_dealloc(ptr: i32, size: i32)`: release a `weaveffi_alloc` block - `weaveffi_error_clear(err_ptr: i32)`: clear and free error resources diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__weaveffi_wasm_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__weaveffi_wasm_d_ts.snap index c619c955..02917f02 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__weaveffi_wasm_d_ts.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__weaveffi_wasm_d_ts.snap @@ -70,6 +70,7 @@ export declare const Priority: Readonly<{ /** High priority */ High: 2; }>; +export type Priority = (typeof Priority)[keyof typeof Priority]; /** A pocket-sized interface exercising the object surface */ export declare class Gadget { diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__weaveffi_wasm_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__weaveffi_wasm_js.snap index 72530846..2b79e996 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__weaveffi_wasm_js.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_kitchen_sink__weaveffi_wasm_js.snap @@ -17,6 +17,19 @@ source: crates/weaveffi-cli/tests/snapshots.rs // Buffered -> records, rich enums, optionals, lists, and maps cross // as one value buffer: i32 pointer + i32 length +// The ABI revision this glue was generated against. +const _ABI_VERSION = 1; + +function _checkAbiVersion(wasm) { + if (typeof wasm.weaveffi_abi_version !== 'function') { + throw new Error(`the loaded WeaveFFI module predates ABI versioning (this glue expects ABI revision ${_ABI_VERSION})`); + } + const found = wasm.weaveffi_abi_version() >>> 0; + if (found !== _ABI_VERSION) { + throw new Error(`WeaveFFI ABI mismatch: this glue expects revision ${_ABI_VERSION} but the loaded module reports revision ${found}`); + } +} + /** Base error for WeaveFFI failures: domain errors extend it, and it is * thrown directly for unknown codes, marshalling failures, and producer * panics. Carries the stable ABI `code`. */ @@ -452,6 +465,8 @@ export async function loadWeaveffiWasm(url) { const { instance } = await WebAssembly.instantiate(bytes, {}); const wasm = instance.exports; + _checkAbiVersion(wasm); + const _table = wasm.__indirect_function_table; let _nextCtxId = 1; diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kvstore__README_md.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kvstore__README_md.snap deleted file mode 100644 index 971028f0..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kvstore__README_md.snap +++ /dev/null @@ -1,241 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- - - - - -# WeaveFFI Wasm (experimental) - -This folder contains a minimal stub to help you load a `wasm32-unknown-unknown` build of your WeaveFFI library. - -Build (example): - -```bash -cargo build --target wasm32-unknown-unknown --release -``` - -Then serve the `.wasm` and use `weaveffi_wasm.js` to load it. - -## Complex Type Handling - -Wasm only supports numeric types natively (`i32`, `i64`, `f32`, `f64`). Complex types are encoded at the boundary as follows: - -### Records - -Records are **plain JavaScript objects**. A record crosses the boundary serialized in the WeaveFFI value-buffer format as one pointer + length pair in linear memory; the glue packs and unpacks it automatically, so there are no handles, no accessor functions, and nothing to free. - -### Enums - -C-style enums are passed as **`i32` values** corresponding to the variant's integer discriminant. Rich (algebraic) enums are plain objects tagged by variant name (`{ tag: "Circle", radius: 2 }`) and cross the boundary serialized in a value buffer, exactly like records. - -### Optionals - -Optional values map to **`null`** for the absent case. An optional crosses the boundary inside a value buffer as a one-byte presence flag followed by the value when present. The one exception is an optional interface, which stays a nullable object pointer (`0` = absent). - -### Lists and Maps - -Lists are JS arrays; maps are plain objects (a `Map` instance is also accepted on input). Both cross the boundary serialized in a value buffer as one pointer + length pair, recursing through element types. - -### Iterators - -`iter` functions return a **lazy JS iterator** (typed `IterableIterator`): each `next()` issues exactly one producer call, so iteration streams in constant memory. The producer handle is destroyed exactly once, on exhaustion or via `return()` when iteration stops early (a `for...of` loop calls `return()` automatically on `break` or `throw`). Abandoning an iterator without exhausting or closing it leaks the handle. - -### Callbacks and Listeners - -Each listener surfaces as a `register.../unregister...` pair. `register` takes a plain JS function and returns a numeric subscription id; `unregister` takes that id and stops delivery. Delivery is **synchronous and same-thread**: `wasm32-unknown-unknown` is single-threaded, so events fire only while a call into the module is on the stack (for example, a producer function that emits during its own execution). A producer that emits from a spawned thread cannot run on this target at all. - -Callback arguments are **borrowed for the duration of the callback**: strings, byte buffers, and buffered values (records, rich enums, optionals, lists, maps) are copied or decoded into JS values before your function runs, and interface arguments wrap producer-owned objects. Read what you need inside the callback and do not retain an interface wrapper or call `free()` on it. - -### Error Handling - -The generated JS wrappers automatically handle errors by passing an error -pointer as the last argument to each Wasm function. The error struct is -16 bytes on wasm32: `{ i32 code, char* message, uint8_t* payload_ptr, -size_t payload_len }`. Your Wasm module must export the following -functions: - -- `weaveffi_alloc(size: i32) -> i32`: allocate `size` bytes in linear memory -- `weaveffi_dealloc(ptr: i32, size: i32)`: release a `weaveffi_alloc` block -- `weaveffi_error_clear(err_ptr: i32)`: clear and free error resources -- `weaveffi_free_string(ptr: i32)`: free a producer-returned C string -- `weaveffi_free_bytes(ptr: i32, len: i32)`: free a producer-returned buffer - -Wrappers of functions declared `throws` raise the declaring module's typed -error class (a `WeaveFFIError` subclass with a per-code subclass, such as -`KeyNotFound`); every other wrapper raises the generic `WeaveFFIError` only -for producer panics and marshalling failures. When the matched error code -declares payload fields, the wrapper decodes them from the error's value -buffer and attaches them as properties on the thrown error. - -## API Reference - -### Module: `kv` - -#### Error Domain: `KvError` - -Throwing wrappers in this module raise `KvError` (a `WeaveFFIError` subclass); each code below is its own subclass carrying the stable `code`. Payload fields are decoded from the error's value buffer and attached as properties on the thrown error. - -| Class | Code | Default Message | Payload Fields | -|-------|------|-----------------|----------------| -| `KeyNotFound` | `1001` | key not found | (none) | -| `Expired` | `1002` | entry expired | (none) | -| `StoreFull` | `1003` | store has reached capacity | (none) | -| `IoError` | `1004` | I/O failure | (none) | - -#### Interfaces - -##### `Store` - -An in-memory key-value store owning its entries; destroying the object releases all of its state - -Passed as an **opaque object pointer** (`i32`), wrapped by a JS class. Constructors return an owned handle; methods pass the handle as the implicit leading `self` argument; `free()` releases the handle via the destroy symbol. - -##### `weaveffi_kv_Store_open` - -Open (or create) a store backed by the given filesystem path - -`weaveffi_kv_Store_open(path: i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `path` | `string` | `i32` | NUL-terminated C string pointer | -| _returns_ | `Store` | `i32` | opaque object pointer | - -##### `weaveffi_kv_Store_put` - -Insert or replace a value, returning true on success - -`weaveffi_kv_Store_put(key: i32, value: i32, i32, kind: i32, ttl_seconds: i32, i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `key` | `string` | `i32` | NUL-terminated C string pointer | -| `value` | `bytes` | `i32, i32` | ptr + len in linear memory | -| `kind` | `EntryKind` | `i32` | variant discriminant | -| `ttl_seconds` | `i64?` | `i32, i32` | value buffer: ptr + len in linear memory | -| _returns_ | `bool` | `i32` | 0 = false, 1 = true | - -##### `weaveffi_kv_Store_get` - -Look up an entry by key; returns null if missing or expired - -`weaveffi_kv_Store_get(key: i32) -> i32, i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `key` | `string` | `i32` | NUL-terminated C string pointer | -| _returns_ | `Entry?` | `i32, i32` | value buffer: ptr + len in linear memory | - -##### `weaveffi_kv_Store_delete` - -Remove the entry for the given key, returning true if it existed - -`weaveffi_kv_Store_delete(key: i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `key` | `string` | `i32` | NUL-terminated C string pointer | -| _returns_ | `bool` | `i32` | 0 = false, 1 = true | - -##### `weaveffi_kv_Store_list_keys` - -Stream every key, optionally filtered by a prefix - -`weaveffi_kv_Store_list_keys(prefix: i32, i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `prefix` | `string?` | `i32, i32` | value buffer: ptr + len in linear memory | -| _returns_ | `iter` | `i32` | opaque iterator handle | - -##### `weaveffi_kv_Store_count` - -Return the number of live entries in the store - -`weaveffi_kv_Store_count() -> i64` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `i64` | `i64` | native Wasm i64 | - -##### `weaveffi_kv_Store_clear` - -Drop every entry from the store - -`weaveffi_kv_Store_clear() -> void` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| - -##### `weaveffi_kv_Store_compact` - -Reclaim space asynchronously; returns the number of bytes reclaimed - -`weaveffi_kv_Store_compact() -> i64` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `i64` | `i64` | native Wasm i64 | - -##### `weaveffi_kv_Store_legacy_put` - -Legacy single-shot put kept for compatibility - -**Deprecated:** use put() with explicit kind - -`weaveffi_kv_Store_legacy_put(key: i32, value: i32, i32) -> i32` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| `key` | `string` | `i32` | NUL-terminated C string pointer | -| `value` | `bytes` | `i32, i32` | ptr + len in linear memory | -| _returns_ | `bool` | `i32` | 0 = false, 1 = true | - -##### `weaveffi_kv_Store_default_capacity` - -The largest number of live entries one store will hold - -`weaveffi_kv_Store_default_capacity() -> i64` - -| Param | API Type | Wasm | Notes | -|-------|----------|------|-------| -| _returns_ | `i64` | `i64` | native Wasm i64 | - -##### `weaveffi_kv_Store_destroy` - -Releases the object reference. Called by the wrapper's `free()`. - -#### Structs - -##### `Entry` - -A single key-value entry persisted in the store - -A plain JS object, serialized in a value buffer (`i32` ptr + `i32` len) at the boundary. Fields in declaration (wire) order: - -| Field | Type | -|-------|------| -| `id` | `i64` | -| `key` | `string` | -| `value` | `bytes` | -| `created_at` | `i64` | -| `expires_at` | `i64?` | -| `tags` | `[string]` | -| `metadata` | `{string:string}` | - -#### Enums - -##### `EntryKind` - -Persistence semantics applied to a stored entry - -Passed as `i32` discriminant. - -| Variant | Value | -|---------|-------| -| `Volatile` | `0` | -| `Persistent` | `1` | -| `Encrypted` | `2` | - - diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kvstore__package_json.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kvstore__package_json.snap deleted file mode 100644 index cee4ead6..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kvstore__package_json.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -{ - "//": "Generated by WeaveFFI [VERSION] from weaveffi.yml", - "//warning": "DO NOT EDIT. Your changes will be overwritten.", - "//regenerate": "To regenerate: weaveffi generate weaveffi.yml -o ", - "name": "weaveffi", - "version": "0.1.0", - "description": "weaveffi bindings generated by WeaveFFI", - "type": "module", - "main": "weaveffi_wasm.js", - "types": "weaveffi_wasm.d.ts" -} diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kvstore__weaveffi_wasm_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kvstore__weaveffi_wasm_d_ts.snap deleted file mode 100644 index ac881a00..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kvstore__weaveffi_wasm_d_ts.snap +++ /dev/null @@ -1,170 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// Generated TypeScript declarations for WeaveFFI Wasm bindings - -/** Base error for WeaveFFI failures: domain errors extend it, and it is - * thrown directly for unknown codes, marshalling failures, and producer - * panics. Carries the stable ABI `code`. */ -export declare class WeaveFFIError extends Error { - constructor(code: number, message?: string); - code: number; -} - -/** Base error for the `kv` module's error domain. */ -export declare class KvError extends WeaveFFIError { - static readonly KeyNotFound: typeof KeyNotFound; - static readonly Expired: typeof Expired; - static readonly StoreFull: typeof StoreFull; - static readonly IoError: typeof IoError; -} - -/** No entry exists for the requested key */ -export declare class KeyNotFound extends KvError { - constructor(message?: string); - static readonly CODE: 1001; -} - -/** The entry exists but its TTL has elapsed */ -export declare class Expired extends KvError { - constructor(message?: string); - static readonly CODE: 1002; -} - -/** The store has reached its capacity limit */ -export declare class StoreFull extends KvError { - constructor(message?: string); - static readonly CODE: 1003; -} - -/** Underlying storage reported an I/O failure */ -export declare class IoError extends KvError { - constructor(message?: string); - static readonly CODE: 1004; -} - -/** A single key-value entry persisted in the store */ -export interface Entry { - /** Stable monotonic identifier assigned on insert */ - id: bigint; - /** UTF-8 lookup key */ - key: string; - /** Opaque binary payload */ - value: Uint8Array; - /** Unix-timestamp seconds when the entry was created */ - created_at: bigint; - /** Optional unix-timestamp seconds at which the entry expires */ - expires_at: bigint | null; - /** Free-form labels attached to the entry */ - tags: string[]; - /** Arbitrary string-valued metadata pairs */ - metadata: Record; -} - -/** Persistence semantics applied to a stored entry */ -export declare const EntryKind: Readonly<{ - /** In-memory only; lost on close */ - Volatile: 0; - /** Flushed to durable storage */ - Persistent: 1; - /** Persistent and encrypted at rest */ - Encrypted: 2; -}>; - -/** An in-memory key-value store owning its entries; destroying the object releases all of its state */ -export declare class Store { - /** - * Open (or create) a store backed by the given filesystem path - * @throws {KvError} on a domain error code - */ - static open(path: string): Store; - /** - * Insert or replace a value, returning true on success - * @throws {KvError} on a domain error code - */ - put(key: string, value: Uint8Array, kind: EntryKind, ttlSeconds: bigint | null): boolean; - /** - * Look up an entry by key; returns null if missing or expired - * @throws {KvError} on a domain error code - */ - get(key: string): Entry | null; - /** - * Remove the entry for the given key, returning true if it existed - * @throws {KvError} on a domain error code - */ - delete(key: string): boolean; - /** - * Stream every key, optionally filtered by a prefix - * @returns A lazy iterator: one producer step per `next()` call. Exhaust it or call `return()` to release the producer handle (a `for...of` loop does both automatically); an abandoned iterator leaks the handle. - * @throws {KvError} on a domain error code - */ - listKeys(prefix: string | null): IterableIterator; - /** - * Return the number of live entries in the store - * @throws {WeaveFFIError} if the native call fails - */ - count(): bigint; - /** - * Drop every entry from the store - * @throws {WeaveFFIError} if the native call fails - */ - clear(): void; - /** - * Reclaim space asynchronously; returns the number of bytes reclaimed - * @throws {KvError} on a domain error code - */ - compact(): Promise; - /** - * Legacy single-shot put kept for compatibility - * @deprecated use put() with explicit kind - * @throws {KvError} on a domain error code - */ - legacyPut(key: string, value: Uint8Array): boolean; - /** - * The largest number of live entries one store will hold - * @throws {WeaveFFIError} if the native call fails - */ - static defaultCapacity(): bigint; - /** Releases the producer-owned handle exactly once. */ - free(): void; -} - -/** Aggregate store statistics */ -export interface Stats { - /** Number of live entries */ - total_entries: bigint; - /** Sum of all value byte lengths */ - total_bytes: bigint; - /** Number of entries past their TTL but not yet evicted */ - expired_entries: bigint; -} - -export interface WeaveffiWasmModule { - _raw: WebAssembly.Exports; - kv: { - /** - * Subscribe to per-key eviction notifications - * - * @returns A subscription id for `unregisterEvictionListener()`. - */ - registerEvictionListener(callback: (key: string) => void): number; - /** Unregister a listener previously registered with `registerEvictionListener()`. */ - unregisterEvictionListener(id: number): void; - Store: typeof Store; - stats: { - /** - * Snapshot the current store statistics - * @throws {KvError} on a domain error code - */ - getStats(store: Store): Stats; - }; - }; -} - -export function loadWeaveffiWasm(url: string): Promise; - -// END weaveffi_wasm.d.ts diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_kvstore__weaveffi_wasm_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_kvstore__weaveffi_wasm_js.snap deleted file mode 100644 index 9a4aa44c..00000000 --- a/crates/weaveffi-cli/tests/snapshots/wasm_kvstore__weaveffi_wasm_js.snap +++ /dev/null @@ -1,661 +0,0 @@ ---- -source: crates/weaveffi-cli/tests/snapshots.rs ---- -// Generated by WeaveFFI [VERSION] from weaveffi.yml -// DO NOT EDIT. Your changes will be overwritten. -// To regenerate: weaveffi generate weaveffi.yml -o - -// WeaveFFI Wasm bindings (auto-generated) -// -// Boundary conventions for a wasm32-unknown-unknown build: -// -// Objects -> i32 pointer into linear memory (0 = null/absent) -// Enums -> i32 discriminant value -// i64/u64 -> JavaScript BigInt -// Strings -> NUL-terminated UTF-8 (const char*); a single i32 pointer -// Bytes -> i32 data pointer + i32 length (out_len for returns) -// Buffered -> records, rich enums, optionals, lists, and maps cross -// as one value buffer: i32 pointer + i32 length - -/** Base error for WeaveFFI failures: domain errors extend it, and it is - * thrown directly for unknown codes, marshalling failures, and producer - * panics. Carries the stable ABI `code`. */ -export class WeaveFFIError extends Error { - constructor(code, message) { - super(message ? `WeaveFFI error ${code}: ${message}` : `WeaveFFI error ${code}`); - this.name = new.target.name; - this.code = code; - } -} - -/** Base error for the `kv` module's error domain. */ -export class KvError extends WeaveFFIError {} - -// No entry exists for the requested key -export class KeyNotFound extends KvError { - constructor(message = "key not found") { - super(1001, message); - } -} -KeyNotFound.CODE = 1001; -KvError.KeyNotFound = KeyNotFound; - -// The entry exists but its TTL has elapsed -export class Expired extends KvError { - constructor(message = "entry expired") { - super(1002, message); - } -} -Expired.CODE = 1002; -KvError.Expired = Expired; - -// The store has reached its capacity limit -export class StoreFull extends KvError { - constructor(message = "store has reached capacity") { - super(1003, message); - } -} -StoreFull.CODE = 1003; -KvError.StoreFull = StoreFull; - -// Underlying storage reported an I/O failure -export class IoError extends KvError { - constructor(message = "I/O failure") { - super(1004, message); - } -} -IoError.CODE = 1004; -KvError.IoError = IoError; - -const _KV_ERROR_CODES = Object.freeze({ - 1001: KeyNotFound, - 1002: Expired, - 1003: StoreFull, - 1004: IoError, -}); - -// Build the KvError subclass matching `code`, or a generic -// WeaveFFIError for codes outside the domain (panics, marshalling). -function _kvErrorFrom(wasm, code, message, payloadPtr, payloadLen) { - const _cls = _KV_ERROR_CODES[code]; - const _e = _cls ? (message ? new _cls(message) : new _cls()) : new WeaveFFIError(code, message); - return _e; -} - -const _enc = new TextEncoder(); -const _dec = new TextDecoder(); - -// Stage a JS string as a NUL-terminated C string in linear memory. -// Returns [ptr, size] (size includes the NUL); release with _free. -function _cstr(wasm, str) { - const bytes = _enc.encode(str); - const size = bytes.length + 1; - const ptr = wasm.weaveffi_alloc(size); - const mem = new Uint8Array(wasm.memory.buffer, ptr, size); - mem.set(bytes); - mem[bytes.length] = 0; - return [ptr, size]; -} - -// Read a NUL-terminated C string (0 => null). Does not free. -function _readCStr(wasm, ptr) { - if (ptr === 0) return null; - const mem = new Uint8Array(wasm.memory.buffer); - let end = ptr; - while (mem[end] !== 0) end++; - return _dec.decode(mem.subarray(ptr, end)); -} - -// Read then free a producer-owned C string. -function _takeCStr(wasm, ptr) { - const s = _readCStr(wasm, ptr); - if (ptr !== 0) wasm.weaveffi_free_string(ptr); - return s; -} - -// Stage a byte buffer (or an encoded value buffer); returns [ptr, len]; -// release with weaveffi_dealloc(ptr, len). -function _bytes(wasm, data) { - const u8 = data instanceof Uint8Array ? data : new Uint8Array(data); - const ptr = wasm.weaveffi_alloc(u8.length); - if (u8.length) new Uint8Array(wasm.memory.buffer, ptr, u8.length).set(u8); - return [ptr, u8.length]; -} - -// Copy then free a producer-owned byte (or value) buffer. -function _takeBytes(wasm, ptr, len) { - if (ptr === 0 || len === 0) return new Uint8Array(0); - const copy = new Uint8Array(wasm.memory.buffer, ptr, len).slice(); - wasm.weaveffi_free_bytes(ptr, len); - return copy; -} - -// Growable little-endian byte writer implementing the WeaveFFI value-buffer -// wire format: values are packed back to back with no alignment; lengths and -// counts are u32; strings are u32 byte length + UTF-8 bytes (no NUL). -class _BufWriter { - constructor() { - this._u8 = new Uint8Array(64); - this._dv = new DataView(this._u8.buffer); - this._len = 0; - } - _need(n) { - if (this._len + n <= this._u8.length) return; - let cap = this._u8.length * 2; - while (cap < this._len + n) cap *= 2; - const u8 = new Uint8Array(cap); - u8.set(this._u8.subarray(0, this._len)); - this._u8 = u8; - this._dv = new DataView(u8.buffer); - } - bool(v) { this._need(1); this._dv.setUint8(this._len, v ? 1 : 0); this._len += 1; } - i8(v) { this._need(1); this._dv.setInt8(this._len, v); this._len += 1; } - u8(v) { this._need(1); this._dv.setUint8(this._len, v); this._len += 1; } - i16(v) { this._need(2); this._dv.setInt16(this._len, v, true); this._len += 2; } - u16(v) { this._need(2); this._dv.setUint16(this._len, v, true); this._len += 2; } - i32(v) { this._need(4); this._dv.setInt32(this._len, v, true); this._len += 4; } - u32(v) { this._need(4); this._dv.setUint32(this._len, v, true); this._len += 4; } - i64(v) { this._need(8); this._dv.setBigInt64(this._len, BigInt(v), true); this._len += 8; } - u64(v) { this._need(8); this._dv.setBigUint64(this._len, BigInt(v), true); this._len += 8; } - f32(v) { this._need(4); this._dv.setFloat32(this._len, v, true); this._len += 4; } - f64(v) { this._need(8); this._dv.setFloat64(this._len, v, true); this._len += 8; } - len(n) { this.u32(n); } - flag(present) { this.u8(present ? 1 : 0); } - str(v) { - const b = _enc.encode(v); - this.len(b.length); - this._need(b.length); - this._u8.set(b, this._len); - this._len += b.length; - } - bytes(v) { - const b = v instanceof Uint8Array ? v : new Uint8Array(v); - this.len(b.length); - this._need(b.length); - this._u8.set(b, this._len); - this._len += b.length; - } - finish() { return this._u8.subarray(0, this._len); } -} - -const _bufDec = new TextDecoder('utf-8', { fatal: true }); - -// Strict little-endian reader for the WeaveFFI value-buffer wire format. A -// malformed buffer (truncation, invalid bool or flag bytes, an oversized -// length prefix, invalid UTF-8, trailing bytes) is a producer/consumer -// contract violation and throws the generic brand error; code -3 marks a -// consumer-side marshalling failure. -class _BufReader { - constructor(bytes) { - this._u8 = bytes; - this._dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); - this._pos = 0; - } - _bad(what) { - throw new WeaveFFIError(-3, 'malformed value buffer: ' + what); - } - _take(n, what) { - if (this._pos + n > this._u8.length) this._bad('truncated ' + what); - const at = this._pos; - this._pos += n; - return at; - } - bool() { - const b = this._u8[this._take(1, 'bool')]; - if (b > 1) this._bad('bool byte out of range'); - return b === 1; - } - i8() { return this._dv.getInt8(this._take(1, 'i8')); } - u8() { return this._u8[this._take(1, 'u8')]; } - i16() { return this._dv.getInt16(this._take(2, 'i16'), true); } - u16() { return this._dv.getUint16(this._take(2, 'u16'), true); } - i32() { return this._dv.getInt32(this._take(4, 'i32'), true); } - u32() { return this._dv.getUint32(this._take(4, 'u32'), true); } - i64() { return this._dv.getBigInt64(this._take(8, 'i64'), true); } - u64() { return this._dv.getBigUint64(this._take(8, 'u64'), true); } - f32() { return this._dv.getFloat32(this._take(4, 'f32'), true); } - f64() { return this._dv.getFloat64(this._take(8, 'f64'), true); } - len() { - const n = this.u32(); - if (n > this._u8.length - this._pos) this._bad('length prefix exceeds remaining buffer'); - return n; - } - flag() { - const b = this._u8[this._take(1, 'option flag')]; - if (b > 1) this._bad('option flag byte out of range'); - return b === 1; - } - str() { - const n = this.len(); - const at = this._take(n, 'string bytes'); - try { - return _bufDec.decode(this._u8.subarray(at, at + n)); - } catch (e) { - this._bad('string is not valid UTF-8'); - } - } - bytes() { - const n = this.len(); - const at = this._take(n, 'byte buffer'); - return this._u8.subarray(at, at + n).slice(); - } - end() { - if (this._pos !== this._u8.length) this._bad('trailing bytes after value'); - } -} - -// Allocate a zeroed 16-byte error slot: -// { i32 code, char* message, uint8_t* payload_ptr, size_t payload_len }. -function _allocErr(wasm) { - const ptr = wasm.weaveffi_alloc(16); - new Uint8Array(wasm.memory.buffer, ptr, 16).fill(0); - return ptr; -} - -// Throw (and free the slot) if the error slot carries a non-zero code. -// Non-throwing wrappers route here: a non-zero code can only be a -// producer panic or a marshalling failure, surfaced as the generic -// WeaveFFIError. -function _checkErr(wasm, errPtr) { - const dv = new DataView(wasm.memory.buffer); - const code = dv.getInt32(errPtr, true); - if (code !== 0) { - const msgPtr = dv.getUint32(errPtr + 4, true); - const msg = _readCStr(wasm, msgPtr) || ''; - wasm.weaveffi_error_clear(errPtr); - wasm.weaveffi_dealloc(errPtr, 16); - throw new WeaveFFIError(code, msg); - } -} - -// Release an error slot on the success path. -function _freeErr(wasm, errPtr) { - wasm.weaveffi_dealloc(errPtr, 16); -} - -// Throw the `KvError` domain error (and free the slot) if the error slot -// carries a non-zero code. -function _checkKvError(wasm, errPtr) { - const dv = new DataView(wasm.memory.buffer); - const code = dv.getInt32(errPtr, true); - if (code !== 0) { - const msg = _readCStr(wasm, dv.getUint32(errPtr + 4, true)) || ''; - const _e = _kvErrorFrom(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)); - wasm.weaveffi_error_clear(errPtr); - wasm.weaveffi_dealloc(errPtr, 16); - throw _e; - } -} - -// Throw if a heap-boxed (consumer-owned) error carries a non-zero -// code. Used by async callbacks: the fields are read, then the box -// is released with weaveffi_error_free before throwing. -// `mkErr` maps domain codes (and decodes payload fields) for -// throwing callables; without it the generic WeaveFFIError is thrown. -function _checkErrRef(wasm, errPtr, mkErr) { - const dv = new DataView(wasm.memory.buffer); - const code = dv.getInt32(errPtr, true); - if (code === 0) { wasm.weaveffi_error_free(errPtr); return; } - const msg = _readCStr(wasm, dv.getUint32(errPtr + 4, true)) || ''; - const err = mkErr ? mkErr(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)) : new WeaveFFIError(code, msg); - wasm.weaveffi_error_free(errPtr); - throw err; -} - -// Serialize a `kv.Entry` record into the value-buffer wire format. -function _write_kv_Entry(w, v) { - w.i64(v.id); - w.str(v.key); - w.bytes(v.value); - w.i64(v.created_at); - if (v.expires_at === null || v.expires_at === undefined) { - w.flag(false); - } else { - w.flag(true); - w.i64(v.expires_at); - } - const _a1 = v.tags || []; - w.len(_a1.length); - for (const _e1 of _a1) { - w.str(_e1); - } - const _s2 = v.metadata || {}; - const _m2 = _s2 instanceof Map ? [..._s2.entries()] : Object.entries(_s2); - w.len(_m2.length); - for (const [_k2, _v2] of _m2) { - w.str(_k2); - w.str(_v2); - } -} - -// Decode a `kv.Entry` record from the value-buffer wire format. -function _read_kv_Entry(r) { - const v = {}; - v.id = r.i64(); - v.key = r.str(); - v.value = r.bytes(); - v.created_at = r.i64(); - v.expires_at = (r.flag() ? r.i64() : null); - v.tags = (() => { const _n = r.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push(r.str()); return _arr; })(); - v.metadata = (() => { const _n = r.len(); const _obj = {}; for (let _i = 0; _i < _n; _i++) { const _k = r.str(); _obj[_k] = r.str(); } return _obj; })(); - return v; -} - -// Serialize a `kv_stats.Stats` record into the value-buffer wire format. -function _write_kv_stats_Stats(w, v) { - w.i64(v.total_entries); - w.i64(v.total_bytes); - w.i64(v.expired_entries); -} - -// Decode a `kv_stats.Stats` record from the value-buffer wire format. -function _read_kv_stats_Stats(r) { - const v = {}; - v.total_entries = r.i64(); - v.total_bytes = r.i64(); - v.expired_entries = r.i64(); - return v; -} - -// Lazy wrapper over a producer iterator handle, implementing the JS -// iterator protocol: each next() issues exactly one producer next call -// and yields one converted element, so iteration streams in constant -// memory. The handle is destroyed exactly once: eagerly on exhaustion, -// on a next error, or from return() when iteration stops early (a -// for...of loop calls return() automatically on break or throw). -// Abandoning an iterator without exhausting or closing it leaks the -// producer handle: JS has no finalization hook that is reliable across -// every target this loader supports. -class _WeaveFFIIterator { - constructor(wasm, handle, slotSize, callNext, destroy, check, decode) { - this._wasm = wasm; - this._handle = handle; - this._slotSize = slotSize; - this._callNext = callNext; - this._destroyFn = destroy; - this._check = check; - this._decode = decode; - this._slot = wasm.weaveffi_alloc(slotSize); - } - // Destroy the handle and release the element slot exactly once. - _close() { - if (this._handle === 0) return; - this._destroyFn(this._handle); - this._handle = 0; - this._wasm.weaveffi_dealloc(this._slot, this._slotSize); - this._slot = 0; - } - next() { - if (this._handle === 0) return { done: true, value: undefined }; - const wasm = this._wasm; - const _err = _allocErr(wasm); - let _has; - try { - _has = this._callNext(this._handle, this._slot, _err); - // Throws (and releases the slot) on a non-zero code. - this._check(wasm, _err); - } catch (e) { - this._close(); - throw e; - } - _freeErr(wasm, _err); - if (_has === 0) { - this._close(); - return { done: true, value: undefined }; - } - return { done: false, value: this._decode(wasm, this._slot) }; - } - // Early-exit cleanup; for...of calls this on break/throw. - return(value) { - this._close(); - return { done: true, value }; - } - [Symbol.iterator]() { - return this; - } -} - -function _registerTrampoline(table, paramTypes, handler) { - const idx = table.grow(1); - table.set(idx, new WebAssembly.Function( - { parameters: paramTypes, results: [] }, - handler - )); - return idx; -} - -export const EntryKind = Object.freeze({ - Volatile: 0, - Persistent: 1, - Encrypted: 2, -}); - -/** - * Load a WeaveFFI Wasm module from the given URL. - * - * @param {string} url - URL to the `.wasm` file. - * @returns {Promise} The API bindings. - * - * Exported functions follow the C ABI naming convention: - * weaveffi_{module}_{function}(params...) -> result - * - * @example - * const api = await loadWeaveffiWasm('lib.wasm'); - * - * // Primitive: plain numbers in, number out. - * const sum = api.math.add(1, 2); - * - * // Record: plain objects in and out (serialized automatically). - * const person = api.contacts.create({ name: 'Ada', age: 36 }); - * - * // Enum: pass the integer discriminant. - * api.ui.set_color(0); // 0 = first variant - * - * // Optional: pass null to omit, a value to provide. - * api.config.set_timeout(5000); // present - * api.config.set_timeout(null); // absent - * - * // List/Map: pass arrays/objects; receive arrays/objects. - * const names = api.data.all_names(); - */ -export async function loadWeaveffiWasm(url) { - const response = await fetch(url); - const bytes = await response.arrayBuffer(); - const { instance } = await WebAssembly.instantiate(bytes, {}); - const wasm = instance.exports; - - const _table = wasm.__indirect_function_table; - - let _nextCtxId = 1; - const _asyncContexts = new Map(); - - function _asyncHandler(ctxId, errPtr, ...results) { - const ctx = _asyncContexts.get(ctxId); - if (!ctx) return; - _asyncContexts.delete(ctxId); - try { - if (errPtr !== 0) _checkErrRef(wasm, errPtr, ctx.mkErr); - ctx.resolve(ctx.unwrap ? ctx.unwrap(wasm, ...results) : results[0]); - } catch (e) { - ctx.reject(e); - } - } - - const _cbPtr_i32_i32_i64 = _registerTrampoline(_table, ['i32', 'i32', 'i64'], _asyncHandler); - - // Listener subscriptions, keyed by the context id the loader - // threads through the C ABI's void* context slot. Each entry - // holds the JS callback and the producer's subscription id. - let _nextLsnId = 1; - const _listeners = new Map(); - - const _lsnPtr_weaveffi_kv_OnEvict_fn = _registerTrampoline(_table, ['i32', 'i32'], (a0, _ctx) => { - const _l = _listeners.get(_ctx); - if (_l === undefined) return; - const _p0 = _readCStr(wasm, a0); - _l.callback(_p0); - }); - - // An in-memory key-value store owning its entries; destroying the object releases all of its state - class Store { - static _wrap(handle) { - const _o = Object.create(Store.prototype); - _o._handle = handle; - return _o; - } - free() { - if (this._handle !== 0) { - wasm.weaveffi_kv_Store_destroy(this._handle); - this._handle = 0; - } - } - static open(path) { - const [a0_p, a0_s] = _cstr(wasm, path); - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_kv_Store_open(a0_p, _err); - wasm.weaveffi_dealloc(a0_p, a0_s); - _checkKvError(wasm, _err); - _freeErr(wasm, _err); - return Store._wrap(_r); - } - put(key, value, kind, ttlSeconds) { - const [a0_p, a0_s] = _cstr(wasm, key); - const [a1_p, a1_l] = _bytes(wasm, value); - const a3_w = new _BufWriter(); - if (ttlSeconds === null || ttlSeconds === undefined) { - a3_w.flag(false); - } else { - a3_w.flag(true); - a3_w.i64(ttlSeconds); - } - const [a3_p, a3_l] = _bytes(wasm, a3_w.finish()); - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_kv_Store_put(this._handle, a0_p, a1_p, a1_l, kind, a3_p, a3_l, _err); - wasm.weaveffi_dealloc(a0_p, a0_s); - wasm.weaveffi_dealloc(a1_p, a1_l); - wasm.weaveffi_dealloc(a3_p, a3_l); - _checkKvError(wasm, _err); - _freeErr(wasm, _err); - return _r !== 0; - } - get(key) { - const [a0_p, a0_s] = _cstr(wasm, key); - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_kv_Store_get(this._handle, a0_p, _lp, _err); - wasm.weaveffi_dealloc(a0_p, a0_s); - _checkKvError(wasm, _err); - _freeErr(wasm, _err); - const _len = new DataView(wasm.memory.buffer).getUint32(_lp, true); - wasm.weaveffi_dealloc(_lp, 4); - const _rd = new _BufReader(_takeBytes(wasm, _r, _len)); - const _out = (_rd.flag() ? _read_kv_Entry(_rd) : null); - _rd.end(); - return _out; - } - delete(key) { - const [a0_p, a0_s] = _cstr(wasm, key); - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_kv_Store_delete(this._handle, a0_p, _err); - wasm.weaveffi_dealloc(a0_p, a0_s); - _checkKvError(wasm, _err); - _freeErr(wasm, _err); - return _r !== 0; - } - listKeys(prefix) { - const a0_w = new _BufWriter(); - if (prefix === null || prefix === undefined) { - a0_w.flag(false); - } else { - a0_w.flag(true); - a0_w.str(prefix); - } - const [a0_p, a0_l] = _bytes(wasm, a0_w.finish()); - const _err = _allocErr(wasm); - const _it = wasm.weaveffi_kv_Store_list_keys(this._handle, a0_p, a0_l, _err); - wasm.weaveffi_dealloc(a0_p, a0_l); - _checkKvError(wasm, _err); - _freeErr(wasm, _err); - return new _WeaveFFIIterator(wasm, _it, 4, - (it, slot, ep) => wasm.weaveffi_kv_Store_ListKeysIterator_next(it, slot, ep), - (it) => wasm.weaveffi_kv_Store_ListKeysIterator_destroy(it), - _checkKvError, (w, p) => _takeCStr(w, new DataView(w.memory.buffer).getUint32(p, true))); - } - count() { - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_kv_Store_count(this._handle, _err); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r; - } - clear() { - const _err = _allocErr(wasm); - wasm.weaveffi_kv_Store_clear(this._handle, _err); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - } - compact() { - return new Promise((resolve, reject) => { - const ctxId = _nextCtxId++; - _asyncContexts.set(ctxId, { resolve, reject, mkErr: _kvErrorFrom }); - wasm.weaveffi_kv_Store_compact_async(this._handle, 0, _cbPtr_i32_i32_i64, ctxId); - }); - } - /** @deprecated use put() with explicit kind */ - legacyPut(key, value) { - const [a0_p, a0_s] = _cstr(wasm, key); - const [a1_p, a1_l] = _bytes(wasm, value); - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_kv_Store_legacy_put(this._handle, a0_p, a1_p, a1_l, _err); - wasm.weaveffi_dealloc(a0_p, a0_s); - wasm.weaveffi_dealloc(a1_p, a1_l); - _checkKvError(wasm, _err); - _freeErr(wasm, _err); - return _r !== 0; - } - static defaultCapacity() { - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_kv_Store_default_capacity(_err); - _checkErr(wasm, _err); - _freeErr(wasm, _err); - return _r; - } - } - - return { - _raw: wasm, - kv: { - /** Subscribe to per-key eviction notifications */ - registerEvictionListener(callback) { - const _id = _nextLsnId++; - const _rid = wasm.weaveffi_kv_register_eviction_listener(_lsnPtr_weaveffi_kv_OnEvict_fn, _id); - _listeners.set(_id, { callback, rid: _rid }); - return _id; - }, - unregisterEvictionListener(id) { - const _l = _listeners.get(id); - if (_l === undefined) return; - _listeners.delete(id); - wasm.weaveffi_kv_unregister_eviction_listener(_l.rid); - }, - Store: Store, - stats: { - getStats(store) { - const _lp = wasm.weaveffi_alloc(4); - const _err = _allocErr(wasm); - const _r = wasm.weaveffi_kv_stats_get_stats(store._handle, _lp, _err); - _checkKvError(wasm, _err); - _freeErr(wasm, _err); - const _len = new DataView(wasm.memory.buffer).getUint32(_lp, true); - wasm.weaveffi_dealloc(_lp, 4); - const _rd = new _BufReader(_takeBytes(wasm, _r, _len)); - const _out = _read_kv_stats_Stats(_rd); - _rd.end(); - return _out; - }, - }, - }, - }; -} - -// END weaveffi_wasm.js diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__README_md.snap b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__README_md.snap index 1f1efc85..06a50f1b 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__README_md.snap @@ -49,6 +49,7 @@ pointer as the last argument to each Wasm function. The error struct is size_t payload_len }`. Your Wasm module must export the following functions: +- `weaveffi_abi_version() -> i32`: the ABI revision the module implements; the loader refuses a mismatch - `weaveffi_alloc(size: i32) -> i32`: allocate `size` bytes in linear memory - `weaveffi_dealloc(ptr: i32, size: i32)`: release a `weaveffi_alloc` block - `weaveffi_error_clear(err_ptr: i32)`: clear and free error resources diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__weaveffi_wasm_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__weaveffi_wasm_d_ts.snap index f47d28bd..72d65d31 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__weaveffi_wasm_d_ts.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__weaveffi_wasm_d_ts.snap @@ -38,6 +38,7 @@ export declare const Unit: Readonly<{ pixels: 0; points: 1; }>; +export type Unit = (typeof Unit)[keyof typeof Unit]; /** A struct one level deep that references both an ancestor enum and a deeper struct. */ export interface Shape { diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__weaveffi_wasm_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__weaveffi_wasm_js.snap index 23b4c6b2..79ea0d39 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__weaveffi_wasm_js.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_nested_modules__weaveffi_wasm_js.snap @@ -17,6 +17,19 @@ source: crates/weaveffi-cli/tests/snapshots.rs // Buffered -> records, rich enums, optionals, lists, and maps cross // as one value buffer: i32 pointer + i32 length +// The ABI revision this glue was generated against. +const _ABI_VERSION = 1; + +function _checkAbiVersion(wasm) { + if (typeof wasm.weaveffi_abi_version !== 'function') { + throw new Error(`the loaded WeaveFFI module predates ABI versioning (this glue expects ABI revision ${_ABI_VERSION})`); + } + const found = wasm.weaveffi_abi_version() >>> 0; + if (found !== _ABI_VERSION) { + throw new Error(`WeaveFFI ABI mismatch: this glue expects revision ${_ABI_VERSION} but the loaded module reports revision ${found}`); + } +} + /** Base error for WeaveFFI failures: domain errors extend it, and it is * thrown directly for unknown codes, marshalling failures, and producer * panics. Carries the stable ABI `code`. */ @@ -338,6 +351,8 @@ export async function loadWeaveffiWasm(url) { const { instance } = await WebAssembly.instantiate(bytes, {}); const wasm = instance.exports; + _checkAbiVersion(wasm); + return { _raw: wasm, graphics: { diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__README_md.snap b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__README_md.snap index ce690f27..a63d5bb8 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__README_md.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__README_md.snap @@ -49,6 +49,7 @@ pointer as the last argument to each Wasm function. The error struct is size_t payload_len }`. Your Wasm module must export the following functions: +- `weaveffi_abi_version() -> i32`: the ABI revision the module implements; the loader refuses a mismatch - `weaveffi_alloc(size: i32) -> i32`: allocate `size` bytes in linear memory - `weaveffi_dealloc(ptr: i32, size: i32)`: release a `weaveffi_alloc` block - `weaveffi_error_clear(err_ptr: i32)`: clear and free error resources diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__weaveffi_wasm_d_ts.snap b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__weaveffi_wasm_d_ts.snap index ce85873f..b1842f0f 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__weaveffi_wasm_d_ts.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__weaveffi_wasm_d_ts.snap @@ -43,6 +43,7 @@ export declare const Channel: Readonly<{ Green: 1; Blue: 2; }>; +export type Channel = (typeof Channel)[keyof typeof Channel]; export interface WeaveffiWasmModule { _raw: WebAssembly.Exports; diff --git a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__weaveffi_wasm_js.snap b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__weaveffi_wasm_js.snap index a60be81e..09c65c9c 100644 --- a/crates/weaveffi-cli/tests/snapshots/wasm_shapes__weaveffi_wasm_js.snap +++ b/crates/weaveffi-cli/tests/snapshots/wasm_shapes__weaveffi_wasm_js.snap @@ -17,6 +17,19 @@ source: crates/weaveffi-cli/tests/snapshots.rs // Buffered -> records, rich enums, optionals, lists, and maps cross // as one value buffer: i32 pointer + i32 length +// The ABI revision this glue was generated against. +const _ABI_VERSION = 1; + +function _checkAbiVersion(wasm) { + if (typeof wasm.weaveffi_abi_version !== 'function') { + throw new Error(`the loaded WeaveFFI module predates ABI versioning (this glue expects ABI revision ${_ABI_VERSION})`); + } + const found = wasm.weaveffi_abi_version() >>> 0; + if (found !== _ABI_VERSION) { + throw new Error(`WeaveFFI ABI mismatch: this glue expects revision ${_ABI_VERSION} but the loaded module reports revision ${found}`); + } +} + /** Base error for WeaveFFI failures: domain errors extend it, and it is * thrown directly for unknown codes, marshalling failures, and producer * panics. Carries the stable ABI `code`. */ @@ -361,6 +374,8 @@ export async function loadWeaveffiWasm(url) { const { instance } = await WebAssembly.instantiate(bytes, {}); const wasm = instance.exports; + _checkAbiVersion(wasm); + return { _raw: wasm, shapes: { diff --git a/crates/weaveffi-core/benches/codegen_bench.rs b/crates/weaveffi-core/benches/codegen_bench.rs index b6da0204..c116dfa4 100644 --- a/crates/weaveffi-core/benches/codegen_bench.rs +++ b/crates/weaveffi-core/benches/codegen_bench.rs @@ -8,9 +8,10 @@ use weaveffi_ir::parse::parse_api_str; fn calculator_api() -> Api { Api { - version: "0.7.0".to_string(), + version: "0.8.0".to_string(), modules: vec![Module { name: "calculator".to_string(), + doc: None, functions: vec![ Function { name: "add".to_string(), @@ -109,8 +110,6 @@ fn calculator_api() -> Api { interfaces: vec![], modules: vec![], }], - generators: None, - package: None, } } @@ -124,6 +123,7 @@ fn large_api() -> Api { .map(|s| StructDef { name: format!("M{m}Struct{s}"), doc: None, + deprecated: None, fields: vec![ StructField { name: "id".to_string(), @@ -148,6 +148,7 @@ fn large_api() -> Api { .map(|e| EnumDef { name: format!("M{m}Enum{e}"), doc: None, + deprecated: None, variants: vec![ EnumVariant { name: "Alpha".to_string(), @@ -208,6 +209,7 @@ fn large_api() -> Api { Module { name: format!("mod{m}"), + doc: None, functions, structs, enums, @@ -221,10 +223,8 @@ fn large_api() -> Api { .collect(); Api { - version: "0.7.0".to_string(), + version: "0.8.0".to_string(), modules, - generators: None, - package: None, } } @@ -259,7 +259,7 @@ fn bench_hash_api(c: &mut Criterion) { /// measures a complete pre-resolved → validated pass on every iteration. fn load_kitchen_sink_unvalidated() -> Api { let path = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../weaveffi-cli/tests/fixtures/06_kitchen_sink.yml"); + .join("../weaveffi-cli/tests/fixtures/kitchen_sink.yml"); let contents = std::fs::read_to_string(&path) .unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display())); parse_api_str(&contents, "yaml") diff --git a/crates/weaveffi-core/src/abi/ctype.rs b/crates/weaveffi-core/src/abi/ctype.rs index cc4dc28f..dfd914fc 100644 --- a/crates/weaveffi-core/src/abi/ctype.rs +++ b/crates/weaveffi-core/src/abi/ctype.rs @@ -111,12 +111,12 @@ impl CType { /// uses, with `prefix` for every WeaveFFI-owned symbol. /// /// This is the Rust counterpart of [`render_c`](Self::render_c): it is the - /// single source of truth for the `weaveffi generate --scaffold` producer - /// stubs, so a scaffolded signature matches the generated C header by - /// construction. Notable lowerings: + /// single source of truth for the `extern "C"` signatures the + /// `#[weaveffi::module]` macro emits, so a macro-generated signature + /// matches the generated C header by construction. Notable lowerings: /// - /// * [`Char`](Self::Char) renders bare as `c_char` (the scaffold imports - /// `std::os::raw::c_char`), and [`Void`](Self::Void) as + /// * [`Char`](Self::Char) renders bare as `c_char` (the emitted code + /// 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 diff --git a/crates/weaveffi-core/src/abi/lower.rs b/crates/weaveffi-core/src/abi/lower.rs index edbe67ad..4ef47a86 100644 --- a/crates/weaveffi-core/src/abi/lower.rs +++ b/crates/weaveffi-core/src/abi/lower.rs @@ -1,21 +1,22 @@ -//! The structural lowering: how each [`TypeRef`] maps onto C ABI parameter -//! and return slots. This is the single source of truth every generator -//! shares. +//! 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 lowering splits every type into one of two families: +//! The lowering dispatches on [`Ty::family`]: //! -//! * **Direct** types occupy dedicated C slots: scalars, bools, and C-style -//! enums by value; strings as `const char*`; bytes as `ptr` + `len`; -//! handles as `uint64_t`; interfaces and iterators as opaque pointers. -//! * **Buffered** types (records, rich enums, optionals, lists, and maps; -//! see [`is_buffered`]) cross as one serialized value buffer: a -//! `const uint8_t*` + `size_t` pair encoded in the WeaveFFI buffer format -//! (`weaveffi-abi`'s `buffer` module). A buffered parameter is borrowed for -//! the call; a buffered return is producer-allocated and released with -//! `{prefix}_free_bytes` after decoding. The single exception is an -//! optional interface, which stays a nullable object pointer. - -use weaveffi_ir::ir::TypeRef; +//! * [`Family::Direct`] types occupy one C slot by value: scalars, bools, +//! C-style enums, and handles. +//! * [`Family::String`] is a `char*`; [`Family::Bytes`] is a `ptr` + `len` +//! pair. +//! * [`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 (`weaveffi-abi`'s +//! `buffer` module). 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 opaque interface pointer, borrowed as a +//! parameter and owned as a return; a nullable one is `Interface?`. + +use crate::model::{Family, Ty}; use super::ctype::{CType, ConstPos}; @@ -49,20 +50,6 @@ pub struct AbiReturn { pub out_params: Vec, } -/// `true` when `ty` crosses the C ABI as a serialized value buffer -/// (`const uint8_t*` + `size_t`) rather than as dedicated C slots. -/// -/// Buffered types are records, rich enums, lists, maps, and optionals, with -/// one exception: an optional *interface* stays a nullable object pointer -/// (an object reference cannot be serialized by value). -pub fn is_buffered(ty: &TypeRef) -> bool { - match ty { - TypeRef::Record(_) | TypeRef::RichEnum(_) | TypeRef::List(_) | TypeRef::Map(_, _) => true, - TypeRef::Optional(inner) => !matches!(inner.as_ref(), TypeRef::Interface(_)), - _ => false, - } -} - /// Split a (possibly qualified) type reference into its C module-path segment /// and bare type name. /// @@ -89,23 +76,28 @@ pub fn struct_tag(name: &str, current_module: &str) -> CType { CType::StructTag { module, name } } -/// Resolve an enum reference (possibly `module.Name`) to its C enum type. -fn enum_ctype(name: &str, current_module: &str) -> CType { - let (module, name) = split_qualified(name, current_module); - CType::Enum { module, name } -} - -/// Resolve a typed-handle reference (possibly `module.Name`) to its C -/// `struct Tag*` pointer type. -fn typed_handle_ctype(name: &str, current_module: &str) -> CType { - let (module, name) = split_qualified(name, current_module); - CType::ptr(CType::StructTag { module, name }) -} - -/// Resolve an interface reference (possibly `module.Name`) to a pointer to its -/// opaque C tag. -fn interface_ptr_ctype(name: &str, current_module: &str) -> CType { - CType::ptr(struct_tag(name, current_module)) +/// The by-value C type of a [`Family::Direct`] type. +fn direct_ctype(ty: &Ty, module: &str) -> CType { + match ty { + Ty::I8 => CType::Int8, + Ty::I16 => CType::Int16, + Ty::I32 => CType::Int32, + Ty::I64 => CType::Int64, + Ty::U8 => CType::Uint8, + Ty::U16 => CType::Uint16, + Ty::U32 => CType::Uint32, + Ty::U64 => CType::Uint64, + Ty::F32 => CType::Float, + Ty::F64 => CType::Double, + Ty::Bool => CType::Bool, + Ty::Handle => CType::Handle, + Ty::TypedHandle(n) => CType::ptr(struct_tag(n, module)), + Ty::Enum(e) => { + let (module, name) = split_qualified(e, module); + CType::Enum { module, name } + } + other => unreachable!("{other} is not a direct type"), + } } /// The two slots of a borrowed buffered parameter: `const uint8_t* {name}_ptr` @@ -117,38 +109,27 @@ fn buffer_param_slots(name: &str) -> Vec { ] } -/// Expand one IR parameter into its ordered C ABI slots. -pub fn lower_param(name: &str, ty: &TypeRef, module: &str, mutable: bool) -> Vec { +/// 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, module: &str, mutable: bool) -> Vec { let west_if_immut = if mutable { ConstPos::None } else { ConstPos::West }; - if is_buffered(ty) { - // A buffered parameter is always an immutable borrow of the encoded - // value; validation rejects `mutable: true` on buffered types. - return buffer_param_slots(name); - } - match ty { - TypeRef::I8 => vec![AbiParam::new(name, CType::Int8)], - TypeRef::I16 => vec![AbiParam::new(name, CType::Int16)], - TypeRef::I32 => vec![AbiParam::new(name, CType::Int32)], - TypeRef::I64 => vec![AbiParam::new(name, CType::Int64)], - TypeRef::U8 => vec![AbiParam::new(name, CType::Uint8)], - TypeRef::U16 => vec![AbiParam::new(name, CType::Uint16)], - TypeRef::U32 => vec![AbiParam::new(name, CType::Uint32)], - TypeRef::U64 => vec![AbiParam::new(name, CType::Uint64)], - TypeRef::F32 => vec![AbiParam::new(name, CType::Float)], - TypeRef::F64 => vec![AbiParam::new(name, CType::Double)], - TypeRef::Bool => vec![AbiParam::new(name, CType::Bool)], - TypeRef::StringUtf8 | TypeRef::BorrowedStr => vec![AbiParam::new( + match ty.family() { + Family::Direct => vec![AbiParam::new(name, direct_ctype(ty, module))], + Family::String => vec![AbiParam::new( name, CType::Ptr { konst: west_if_immut, pointee: Box::new(CType::Char), }, )], - TypeRef::Bytes | TypeRef::BorrowedBytes => vec![ + Family::Bytes => vec![ AbiParam::new( format!("{name}_ptr"), CType::Ptr { @@ -158,72 +139,57 @@ pub fn lower_param(name: &str, ty: &TypeRef, module: &str, mutable: bool) -> Vec ), AbiParam::new(format!("{name}_len"), CType::Size), ], - TypeRef::Handle => vec![AbiParam::new(name, CType::Handle)], - TypeRef::TypedHandle(n) => vec![AbiParam::new(name, typed_handle_ctype(n, module))], - TypeRef::Named(n) => unreachable!("unresolved type reference '{n}' reached ABI lowering"), + // A buffered parameter is always an immutable borrow of the encoded + // value; validation rejects `mutable: true` on buffered types. + Family::Buffer => buffer_param_slots(name), // An interface parameter borrows the object for the call: the callee - // reads through the const pointer and never takes ownership. - TypeRef::Interface(i) => vec![AbiParam::new( - name, - CType::Ptr { - konst: ConstPos::West, - pointee: Box::new(struct_tag(i, module)), - }, - )], - TypeRef::Enum(e) => vec![AbiParam::new(name, enum_ctype(e, module))], - // Only `Interface?` reaches here (every other optional is buffered): - // a nullable borrowed object pointer, null meaning none. - TypeRef::Optional(inner) => lower_param(name, inner, module, mutable), - TypeRef::Record(_) | TypeRef::RichEnum(_) | TypeRef::List(_) | TypeRef::Map(_, _) => { - unreachable!("buffered type handled above") + // reads through the const pointer and never takes ownership. 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, module)), + }, + )] } - TypeRef::Iterator(_) => unreachable!("iterator not valid as parameter"), + Family::Iterator => unreachable!("iterator not valid as parameter"), } } /// Lower a return type to its C return type plus trailing out-parameters. -pub fn lower_return(ty: &TypeRef, module: &str) -> AbiReturn { +/// +/// # Panics +/// +/// Panics on an iterator type, whose launcher is lowered by the function +/// lowering in [`crate::model`] rather than as a plain value return. +pub fn lower_return(ty: &Ty, module: &str) -> AbiReturn { let no_out = |ret| AbiReturn { ret, out_params: vec![], }; - if is_buffered(ty) { - // A buffered return is producer-allocated, exactly like a bytes - // return: the caller decodes it and then calls `{prefix}_free_bytes`. - return AbiReturn { - ret: CType::const_ptr(CType::Uint8), - out_params: vec![AbiParam::new("out_len", CType::ptr(CType::Size))], - }; - } - match ty { - TypeRef::I8 => no_out(CType::Int8), - TypeRef::I16 => no_out(CType::Int16), - TypeRef::I32 => no_out(CType::Int32), - TypeRef::I64 => no_out(CType::Int64), - TypeRef::U8 => no_out(CType::Uint8), - TypeRef::U16 => no_out(CType::Uint16), - TypeRef::U32 => no_out(CType::Uint32), - TypeRef::U64 => no_out(CType::Uint64), - TypeRef::F32 => no_out(CType::Float), - TypeRef::F64 => no_out(CType::Double), - TypeRef::Bool => no_out(CType::Bool), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => no_out(CType::const_ptr(CType::Char)), - TypeRef::Bytes | TypeRef::BorrowedBytes => AbiReturn { + match ty.family() { + Family::Direct => no_out(direct_ctype(ty, module)), + Family::String => no_out(CType::const_ptr(CType::Char)), + // A buffered return is producer-allocated exactly like a bytes return: + // the caller decodes it and then calls `{prefix}_free_bytes`. + Family::Bytes | Family::Buffer => AbiReturn { ret: CType::const_ptr(CType::Uint8), out_params: vec![AbiParam::new("out_len", CType::ptr(CType::Size))], }, - TypeRef::Handle => no_out(CType::Handle), - TypeRef::TypedHandle(n) => no_out(typed_handle_ctype(n, module)), - TypeRef::Named(n) => unreachable!("unresolved type reference '{n}' reached ABI lowering"), - // A returned interface transfers ownership of a new object reference. - TypeRef::Interface(i) => no_out(interface_ptr_ctype(i, module)), - TypeRef::Enum(e) => no_out(enum_ctype(e, module)), - // Only `Interface?` reaches here: a nullable owned object pointer. - TypeRef::Optional(inner) => lower_return(inner, module), - TypeRef::Record(_) | TypeRef::RichEnum(_) | TypeRef::List(_) | TypeRef::Map(_, _) => { - unreachable!("buffered type handled above") + // A returned interface transfers ownership of a new object 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, module))) } - TypeRef::Iterator(_) => { + Family::Iterator => { unreachable!("iterator return handled specially by the function lowering") } } @@ -232,25 +198,20 @@ pub fn lower_return(ty: &TypeRef, module: &str) -> AbiReturn { /// The trailing result fields appended to an async callback after the /// `(context, err)` prefix. /// -/// Bytes and buffered results are passed as a borrowed `ptr` + `len` pair -/// (the producer owns the buffer for the callback's duration); everything -/// else reuses its return slot type by value. -pub fn callback_result_params(ty: &TypeRef, module: &str) -> Vec { - if is_buffered(ty) { - return vec![ +/// 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, module: &str) -> Vec { + match ty.family() { + Family::Buffer => vec![ AbiParam::new("result_ptr", CType::const_ptr(CType::Uint8)), AbiParam::new("result_len", CType::Size), - ]; - } - match ty { - TypeRef::Bytes | TypeRef::BorrowedBytes => vec![ + ], + Family::Bytes => vec![ AbiParam::new("result", CType::const_ptr(CType::Uint8)), AbiParam::new("result_len", CType::Size), ], - _ => { - let ret = lower_return(ty, module).ret; - vec![AbiParam::new("result", ret)] - } + _ => vec![AbiParam::new("result", lower_return(ty, module).ret)], } } @@ -266,176 +227,120 @@ mod tests { } #[test] - fn scalar_param() { + fn params_lower_by_family() { assert_eq!( - render(&lower_param("x", &TypeRef::I32, "m", false)), + render(&lower_param("x", &Ty::I32, "m", false)), ["int32_t x"] ); - } - - #[test] - fn string_param_is_const_unless_mutable() { assert_eq!( - render(&lower_param("s", &TypeRef::StringUtf8, "m", false)), + render(&lower_param("s", &Ty::StringUtf8, "m", false)), ["const char* s"] ); assert_eq!( - render(&lower_param("s", &TypeRef::StringUtf8, "m", true)), + render(&lower_param("s", &Ty::StringUtf8, "m", true)), ["char* s"] ); - } - - #[test] - fn bytes_param_expands_to_ptr_and_len() { assert_eq!( - render(&lower_param("data", &TypeRef::Bytes, "m", false)), + render(&lower_param("data", &Ty::Bytes, "m", false)), ["const uint8_t* data_ptr", "size_t data_len"] ); - } - - #[test] - fn buffered_kinds_are_detected() { - assert!(is_buffered(&TypeRef::Record("Contact".into()))); - assert!(is_buffered(&TypeRef::RichEnum("Shape".into()))); - assert!(is_buffered(&TypeRef::List(Box::new(TypeRef::I32)))); - assert!(is_buffered(&TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32) - ))); - assert!(is_buffered(&TypeRef::Optional(Box::new(TypeRef::I32)))); - assert!(is_buffered(&TypeRef::Optional(Box::new( - TypeRef::StringUtf8 - )))); - // The one optional exception: nullable interface pointers. - assert!(!is_buffered(&TypeRef::Optional(Box::new( - TypeRef::Interface("Store".into()) - )))); - assert!(!is_buffered(&TypeRef::I32)); - assert!(!is_buffered(&TypeRef::StringUtf8)); - assert!(!is_buffered(&TypeRef::Bytes)); - assert!(!is_buffered(&TypeRef::Interface("Store".into()))); - assert!(!is_buffered(&TypeRef::Enum("Color".into()))); - } - - #[test] - fn list_param_is_one_buffer() { - let xs = TypeRef::List(Box::new(TypeRef::I32)); assert_eq!( - render(&lower_param("xs", &xs, "m", false)), + render(&lower_param("xs", &Ty::List(Box::new(Ty::I32)), "m", false)), ["const uint8_t* xs_ptr", "size_t xs_len"] ); - } - - #[test] - fn record_param_is_one_buffer() { - let p = lower_param("c", &TypeRef::Record("other.Contact".into()), "ops", false); - assert_eq!(render(&p), ["const uint8_t* c_ptr", "size_t c_len"]); - } - - #[test] - fn optional_scalar_is_buffered() { - let o = TypeRef::Optional(Box::new(TypeRef::I32)); assert_eq!( - render(&lower_param("x", &o, "m", false)), - ["const uint8_t* x_ptr", "size_t x_len"] + render(&lower_param( + "c", + &Ty::Record("other.Contact".into()), + "ops", + false + )), + ["const uint8_t* c_ptr", "size_t c_len"] ); - } - - #[test] - fn optional_interface_is_nullable_pointer() { - let o = TypeRef::Optional(Box::new(TypeRef::Interface("Store".into()))); assert_eq!( - render(&lower_param("s", &o, "kv", false)), + render(&lower_param( + "s", + &Ty::Optional(Box::new(Ty::Interface("Store".into()))), + "kv", + false + )), ["const weaveffi_kv_Store* s"] ); - let r = lower_return(&o, "kv"); - assert_eq!(r.ret.render_c("weaveffi"), "weaveffi_kv_Store*"); - assert!(r.out_params.is_empty()); - } - - #[test] - fn map_param_is_one_buffer() { - let m = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)); assert_eq!( - render(&lower_param("m", &m, "mod", false)), - ["const uint8_t* m_ptr", "size_t m_len"] + render(&lower_param( + "s", + &Ty::Enum("shared.Status".into()), + "orders", + false + )), + ["weaveffi_shared_Status s"] + ); + assert_eq!( + render(&lower_param( + "h", + &Ty::TypedHandle("auth.Session".into()), + "api", + false + )), + ["weaveffi_auth_Session* h"] ); } #[test] - fn bytes_return_has_out_len() { - let r = lower_return(&TypeRef::Bytes, "m"); + fn returns_lower_by_family() { + let r = lower_return(&Ty::Bytes, "m"); assert_eq!(r.ret.render_c("weaveffi"), "const uint8_t*"); assert_eq!(render(&r.out_params), ["size_t* out_len"]); - } - - #[test] - fn buffered_returns_share_the_bytes_shape() { for ty in [ - TypeRef::Record("Contact".into()), - TypeRef::RichEnum("Shape".into()), - TypeRef::List(Box::new(TypeRef::Record("Contact".into()))), - TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - TypeRef::Optional(Box::new(TypeRef::I64)), - TypeRef::List(Box::new(TypeRef::List(Box::new(TypeRef::I32)))), + Ty::Record("Contact".into()), + Ty::RichEnum("Shape".into()), + Ty::List(Box::new(Ty::Record("Contact".into()))), + Ty::Map(Box::new(Ty::StringUtf8), Box::new(Ty::I32)), + Ty::Optional(Box::new(Ty::I64)), ] { let r = lower_return(&ty, "m"); - assert_eq!(r.ret.render_c("weaveffi"), "const uint8_t*", "{ty:?}"); - assert_eq!(render(&r.out_params), ["size_t* out_len"], "{ty:?}"); + assert_eq!(r.ret.render_c("weaveffi"), "const uint8_t*", "{ty}"); + assert_eq!(render(&r.out_params), ["size_t* out_len"], "{ty}"); } + let r = lower_return(&Ty::Optional(Box::new(Ty::Interface("Store".into()))), "kv"); + assert_eq!(r.ret.render_c("weaveffi"), "weaveffi_kv_Store*"); + assert!(r.out_params.is_empty()); + assert_eq!( + lower_return(&Ty::Enum("shared.Status".into()), "orders") + .ret + .render_c("weaveffi"), + "weaveffi_shared_Status" + ); } #[test] - fn callback_buffered_result_is_borrowed_pair() { - let params = callback_result_params(&TypeRef::List(Box::new(TypeRef::StringUtf8)), "m"); + fn callback_results() { assert_eq!( - render(¶ms), + render(&callback_result_params( + &Ty::List(Box::new(Ty::StringUtf8)), + "m" + )), ["const uint8_t* result_ptr", "size_t result_len"] ); + assert_eq!( + render(&callback_result_params(&Ty::Bytes, "m")), + ["const uint8_t* result", "size_t result_len"] + ); + assert_eq!( + render(&callback_result_params(&Ty::I32, "m")), + ["int32_t result"] + ); } #[test] fn split_qualified_handles_levels() { - // Unqualified -> belongs to current module. assert_eq!( split_qualified("Name", "current"), ("current".to_string(), "Name".to_string()) ); - // Single-level qualified. - assert_eq!( - split_qualified("shared.Status", "orders"), - ("shared".to_string(), "Status".to_string()) - ); - // Multi-level qualified: only the final segment is the type name; the - // dotted module path flattens to an underscore-joined C prefix. assert_eq!( split_qualified("a.b.c.Name", "x"), ("a_b_c".to_string(), "Name".to_string()) ); } - - #[test] - fn cross_module_enum_param_resolves_module() { - // Regression: a sibling-module enum must render `weaveffi__`, - // never `weaveffi__.`. - let p = lower_param("s", &TypeRef::Enum("shared.Status".into()), "orders", false); - assert_eq!(render(&p), ["weaveffi_shared_Status s"]); - } - - #[test] - fn cross_module_enum_return_resolves_module() { - let r = lower_return(&TypeRef::Enum("shared.Status".into()), "orders"); - assert_eq!(r.ret.render_c("weaveffi"), "weaveffi_shared_Status"); - } - - #[test] - fn cross_module_typed_handle_param_resolves_module() { - let p = lower_param( - "h", - &TypeRef::TypedHandle("auth.Session".into()), - "api", - false, - ); - assert_eq!(render(&p), ["weaveffi_auth_Session* h"]); - } } diff --git a/crates/weaveffi-core/src/abi/mod.rs b/crates/weaveffi-core/src/abi/mod.rs index 84c39351..d6d2a958 100644 --- a/crates/weaveffi-core/src/abi/mod.rs +++ b/crates/weaveffi-core/src/abi/mod.rs @@ -1,28 +1,26 @@ //! The canonical WeaveFFI C ABI model. //! -//! This module is the single source of truth for *how a validated `Api` -//! lowers onto the stable C ABI*: which symbols exist, the exact ordered -//! parameter list of each, and how every [`TypeRef`] crosses the boundary -//! (by value, as a pointer, as `ptr`+`len`, as a serialized value buffer, -//! with a trailing `out_err`, …). +//! This module is the single source of truth for *how a resolved API lowers +//! onto the stable C ABI*: which symbols exist, the exact ordered parameter +//! list of each, and how every [`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). //! -//! Before this module existed, each of the eleven language generators *and* -//! the Rust scaffold re-derived the calling convention independently, kept in -//! sync only by snapshot tests. They now share [`lower_param`], -//! [`lower_return`], [`is_buffered`], [`callback_result_params`], and the -//! signature assembly helpers below, and map the resulting [`CType`] onto -//! their own FFI vocabulary. The C rendering ([`CType::render_c`]) is the -//! canonical one. +//! Every language generator and the producer macro share [`lower_param`], +//! [`lower_return`], [`callback_result_params`], and the signature assembly +//! helpers below, and map the resulting [`CType`] onto their own FFI +//! vocabulary. The C rendering ([`CType::render_c`]) is the canonical one. pub mod ctype; pub mod lower; pub use ctype::{CType, ConstPos}; pub use lower::{ - callback_result_params, is_buffered, lower_param, lower_return, struct_tag, AbiParam, AbiReturn, + callback_result_params, lower_param, lower_return, split_qualified, struct_tag, AbiParam, + AbiReturn, }; -use weaveffi_ir::ir::{Function, Param, TypeRef}; +use crate::model::{ParamBinding, Ty}; /// A fully-assembled C ABI signature: the ordered parameter slots and the /// C return type. @@ -51,12 +49,9 @@ pub fn cancel_token_param() -> AbiParam { } /// Assemble the full C signature of a *synchronous* function: every input -/// parameter, then the return type's out-parameters, then `out_err`. -pub fn sync_signature(params: &[Param], returns: Option<&TypeRef>, module: &str) -> AbiSig { - let mut out = Vec::new(); - for p in params { - out.extend(lower_param(&p.name, &p.ty, module, p.mutable)); - } +/// parameter's slots, then the return type's out-parameters, then `out_err`. +pub fn sync_signature(params: &[ParamBinding], returns: Option<&Ty>, module: &str) -> AbiSig { + let mut out: Vec = params.iter().flat_map(|p| p.abi.iter().cloned()).collect(); let ret = match returns { Some(ty) => { let r = lower_return(ty, module); @@ -71,7 +66,7 @@ pub fn sync_signature(params: &[Param], returns: Option<&TypeRef>, module: &str) /// Assemble the parameters of the async completion callback typedef: /// `(void* context, {prefix}_error* err, )`. -pub fn async_callback_params(returns: Option<&TypeRef>, module: &str) -> Vec { +pub fn async_callback_params(returns: Option<&Ty>, module: &str) -> Vec { let mut params = vec![ context_param(), AbiParam::new("err", CType::ptr(CType::Error)), @@ -86,29 +81,20 @@ pub fn async_callback_params(returns: Option<&TypeRef>, module: &str) -> Vec Vec { - let mut params = Vec::new(); - for p in &f.params { - params.extend(lower_param(&p.name, &p.ty, module, p.mutable)); - } - if f.cancellable { - params.push(cancel_token_param()); +pub fn async_input_params(params: &[ParamBinding], cancellable: bool) -> Vec { + let mut out: Vec = params.iter().flat_map(|p| p.abi.iter().cloned()).collect(); + if cancellable { + out.push(cancel_token_param()); } - params + out } #[cfg(test)] mod tests { use super::*; - use weaveffi_ir::ir::TypeRef; - fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - mutable: false, - doc: None, - } + fn param(name: &str, ty: Ty) -> ParamBinding { + ParamBinding::new(name, ty, false, None, "math") } fn render(params: &[AbiParam]) -> Vec { @@ -121,8 +107,8 @@ mod tests { #[test] fn sync_signature_appends_out_err_last() { let sig = sync_signature( - &[param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(&TypeRef::I32), + &[param("a", Ty::I32), param("b", Ty::I32)], + Some(&Ty::I32), "math", ); assert_eq!(sig.ret, CType::Int32); @@ -130,34 +116,27 @@ mod tests { render(&sig.params), ["int32_t a", "int32_t b", "weaveffi_error* out_err"] ); - } - - #[test] - fn sync_signature_void_return() { - let sig = sync_signature(&[param("x", TypeRef::I32)], None, "m"); - assert_eq!(sig.ret, CType::Void); - assert_eq!( - render(&sig.params), - ["int32_t x", "weaveffi_error* out_err"] - ); - } - - #[test] - fn sync_signature_bytes_return_inserts_out_len_before_out_err() { - let sig = sync_signature(&[], Some(&TypeRef::Bytes), "m"); + let sig = sync_signature(&[], Some(&Ty::Bytes), "m"); assert_eq!(sig.ret.render_c("weaveffi"), "const uint8_t*"); assert_eq!( render(&sig.params), ["size_t* out_len", "weaveffi_error* out_err"] ); + let sig = sync_signature(&[param("x", Ty::I32)], None, "m"); + assert_eq!(sig.ret, CType::Void); } #[test] - fn async_callback_prefix_is_context_then_err() { - let params = async_callback_params(Some(&TypeRef::I32), "m"); + fn async_shapes() { + let params = async_callback_params(Some(&Ty::I32), "m"); assert_eq!( render(¶ms), ["void* context", "weaveffi_error* err", "int32_t result"] ); + let inputs = async_input_params(&[param("id", Ty::I64)], true); + assert_eq!( + render(&inputs), + ["int64_t id", "weaveffi_cancel_token* cancel_token"] + ); } } diff --git a/crates/weaveffi-core/src/backend/mod.rs b/crates/weaveffi-core/src/backend/mod.rs index 971c45ba..16355a3f 100644 --- a/crates/weaveffi-core/src/backend/mod.rs +++ b/crates/weaveffi-core/src/backend/mod.rs @@ -1,23 +1,23 @@ //! The language-backend framework. //! //! Every idiomatic WeaveFFI generator does the same three things: it walks the -//! [`BindingModel`] in a fixed order (enums → structs → callbacks → listeners -//! → functions), dispatches each function on its [`CallShape`], and writes a -//! primary source file plus a handful of package manifests. Before this module -//! existed, all eleven generators hand-rolled that walk, that dispatch, that -//! file I/O, and their own copy of the [`Generator`] glue; they drifted. +//! [`BindingModel`] in a fixed order (errors, enums, structs, interfaces, +//! callbacks, listeners, functions), dispatches each function on its +//! [`CallShape`], and writes a primary source file plus a handful of package +//! manifests. //! -//! [`LanguageBackend`] captures the common structure as a trait whose hooks a -//! backend implements, and the free [`run`]/[`output_files`] functions plus the -//! [`impl_generator_via_backend!`](crate::impl_generator_via_backend) macro provide the shared driver. A backend -//! now owns *only* language-specific rendering: type mapping, marshalling, and -//! the exact text of each declaration. The traversal order, the call-shape -//! dispatch, the model construction, and the bridge to the object-safe -//! [`Generator`]/`DynGenerator` layer all live here, once. +//! [`LanguageBackend`] is the one trait a target implements. It captures the +//! common structure as hooks, and the free [`run`], [`output_files`], and +//! [`package_files`] functions provide the shared driver that +//! [`ConfiguredBackend`](crate::codegen::ConfiguredBackend) exposes to the +//! orchestrator through the object-safe [`Target`](crate::codegen::Target) +//! trait. A backend owns *only* language-specific rendering: type mapping, +//! marshalling, and the exact text of each declaration. The traversal order, +//! the call-shape dispatch, the model construction, and the erasure all live +//! here, once. //! //! [`BindingModel`]: crate::model::BindingModel //! [`CallShape`]: crate::model::CallShape -//! [`Generator`]: crate::codegen::Generator use anyhow::Result; use camino::{Utf8Path, Utf8PathBuf}; @@ -54,12 +54,13 @@ impl OutputFile { /// An idiomatic language backend over the shared [`BindingModel`]. /// -/// The single required method is [`files`](Self::files), which assembles the -/// complete output set; pair it with [`impl_generator_via_backend!`](crate::impl_generator_via_backend) to wire -/// the type into the [`Generator`](crate::codegen::Generator) trait the CLI and -/// orchestrator consume. That alone gives every backend the shared driver, the -/// [`OutputFile`] model (rendering is pure; the driver does the I/O), an -/// automatically-derived `output_files`, and one uniform `Generator` bridge. +/// The required methods are [`name`](Self::name), +/// [`capabilities`](Self::capabilities), and [`files`](Self::files), which +/// assembles the complete output set; wrap the type in +/// [`ConfiguredBackend`](crate::codegen::ConfiguredBackend) to hand it to the +/// orchestrator. That alone gives every backend the shared driver, the +/// [`OutputFile`] model (rendering is pure; the driver does the I/O), and an +/// automatically-derived `output_files`. /// /// Backends whose primary file is a straightforward per-module walk (Python, /// Ruby, Go, Dart, .NET) override the per-entity hooks (`render_enum`, @@ -80,11 +81,12 @@ impl OutputFile { /// to centralise here, but every backend shares /// [`emit_doc`](crate::codegen::common::emit_doc) for the line/block flavours. pub trait LanguageBackend: Send + Sync { - /// Per-target, fully-typed configuration. Mirrors - /// [`Generator::Config`](crate::codegen::Generator::Config). + /// Per-target, fully-typed configuration. Must round-trip through + /// `serde_json` so the orchestrator can hash it as part of the cache key. type Config: Serialize + Default + Clone + Send + Sync; - /// Stable short name (`"swift"`, `"python"`, …): the `--target` token. + /// Stable short name (`"swift"`, `"python"`, ...): the `--target` token + /// and the cache file basename. fn name(&self) -> &'static str; /// The gated IDL features this backend implements (async functions, @@ -93,9 +95,12 @@ pub trait LanguageBackend: Send + Sync { /// backend silently skipping a feature it never implemented. fn capabilities(&self) -> TargetCapabilities; - /// Whether the bound config explicitly opted in to generating despite - /// unsupported features (see - /// [`Generator::allows_unsupported`](crate::codegen::Generator::allows_unsupported)). + /// Whether the user explicitly opted in to generating this target even + /// though the API uses features the target does not support (via an + /// `allow_unsupported = true` flag in the target's config). When `true` + /// the orchestrator downgrades the capability failure to a loud warning + /// and the backend must emit an explicit unsupported surface (throwing + /// stubs, documentation) rather than silently omitting the feature. /// Backends with partial capabilities override this to read their /// `allow_unsupported` config flag; full-capability backends keep the /// `false` default. @@ -267,10 +272,8 @@ pub trait LanguageBackend: Send + Sync { } } -/// Build the model and write every file a backend produces. -/// -/// This is the body of the [`Generator::generate`](crate::codegen::Generator) -/// impl that [`impl_generator_via_backend!`](crate::impl_generator_via_backend) generates. +/// Build the model and write every file a backend produces. This is the body +/// of [`Target::generate`](crate::codegen::Target::generate). /// /// # Errors /// @@ -308,11 +311,10 @@ fn forward_slashes(path: Utf8PathBuf) -> String { } } -/// The sorted list of paths a backend would write, the body of the -/// [`Generator::output_files`](crate::codegen::Generator::output_files) impl -/// that [`impl_generator_via_backend!`](crate::impl_generator_via_backend) generates. Used by `--dry-run` and -/// `weaveffi diff`. Paths are normalised to `/` separators so the listing is -/// identical across operating systems. +/// The sorted list of paths a backend would write, the body of +/// [`Target::output_files`](crate::codegen::Target::output_files). Used by +/// `--dry-run` and `weaveffi diff`. Paths are normalised to `/` separators so +/// the listing is identical across operating systems. pub fn output_files( backend: &B, api: &ResolvedApi, @@ -330,9 +332,8 @@ pub fn output_files( } /// Build the model and assemble the package a backend produces, the body of -/// the [`Generator::package`](crate::codegen::Generator::package) impl that -/// [`impl_generator_via_backend!`](crate::impl_generator_via_backend) -/// generates. Returns `None` when the backend does not support packaging. +/// [`Target::package`](crate::codegen::Target::package). Returns `None` when +/// the backend does not support packaging. pub fn package_files( backend: &B, api: &ResolvedApi, @@ -344,74 +345,10 @@ pub fn package_files( backend.package(api, &model, ctx, out_dir, config) } -/// Re-export of `anyhow` so [`impl_generator_via_backend!`](crate::impl_generator_via_backend) -/// can name the `Generator::generate` return type in its expansion without -/// forcing every backend crate to declare a direct `anyhow` dependency it never -/// references in its own source. Not part of the public API. -#[doc(hidden)] -pub use anyhow as __anyhow; - -/// Implement the object-safe [`Generator`](crate::codegen::Generator) trait for -/// a type that implements [`LanguageBackend`], delegating to the shared driver. -/// -/// ```ignore -/// pub struct PythonGenerator; -/// impl weaveffi_core::backend::LanguageBackend for PythonGenerator { /* … */ } -/// weaveffi_core::impl_generator_via_backend!(PythonGenerator); -/// ``` -#[macro_export] -macro_rules! impl_generator_via_backend { - ($backend:ty) => { - impl $crate::codegen::Generator for $backend { - type Config = <$backend as $crate::backend::LanguageBackend>::Config; - - fn name(&self) -> &'static str { - <$backend as $crate::backend::LanguageBackend>::name(self) - } - - fn capabilities(&self) -> $crate::capabilities::TargetCapabilities { - <$backend as $crate::backend::LanguageBackend>::capabilities(self) - } - - fn allows_unsupported(&self, config: &Self::Config) -> bool { - <$backend as $crate::backend::LanguageBackend>::allows_unsupported(self, config) - } - - fn generate( - &self, - api: &$crate::resolved::ResolvedApi, - out_dir: &::camino::Utf8Path, - config: &Self::Config, - ) -> $crate::backend::__anyhow::Result<()> { - $crate::backend::run(self, api, out_dir, config) - } - - fn output_files( - &self, - api: &$crate::resolved::ResolvedApi, - out_dir: &::camino::Utf8Path, - config: &Self::Config, - ) -> ::std::vec::Vec<::std::string::String> { - $crate::backend::output_files(self, api, out_dir, config) - } - - fn package( - &self, - api: &$crate::resolved::ResolvedApi, - ctx: &$crate::package::PackageContext, - out_dir: &::camino::Utf8Path, - config: &Self::Config, - ) -> ::core::option::Option<::std::vec::Vec<$crate::package::PackagedFile>> { - $crate::backend::package_files(self, api, ctx, out_dir, config) - } - } - }; -} - #[cfg(test)] mod tests { use super::*; - use crate::codegen::Generator; + use crate::model::CallShape; use weaveffi_ir::ir::{Api, Function, Module, Param, TypeRef}; #[derive(Default, Clone, serde::Serialize)] @@ -438,20 +375,6 @@ mod tests { config.prefix.as_deref().unwrap_or("weaveffi") } - fn render_enum(&self, out: &mut String, e: &EnumBinding, _c: &Self::Config) { - out.push_str(&format!("enum {}\n", e.name)); - } - - fn render_struct( - &self, - out: &mut String, - _m: &ModuleBinding, - s: &StructBinding, - _c: &Self::Config, - ) { - out.push_str(&format!("struct {}\n", s.name)); - } - fn render_function( &self, out: &mut String, @@ -460,9 +383,9 @@ mod tests { _c: &Self::Config, ) { let shape = match &f.shape { - crate::model::CallShape::Sync(_) => "sync", - crate::model::CallShape::Async(_) => "async", - crate::model::CallShape::Iterator(_) => "iter", + CallShape::Sync(_) => "sync", + CallShape::Async(_) => "async", + CallShape::Iterator(_) => "iter", }; out.push_str(&format!("fn {} [{}] {}\n", f.name, shape, f.c_base)); } @@ -503,13 +426,19 @@ mod tests { } fn api() -> ResolvedApi { - ResolvedApi::assume_resolved(Api { + ResolvedApi::assume_valid(Api { version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), modules: vec![Module { name: "math".into(), + doc: None, functions: vec![ func("add", Some(TypeRef::I32), false), func("fetch", Some(TypeRef::StringUtf8), true), + func( + "scan", + Some(TypeRef::Iterator(Box::new(TypeRef::I32))), + false, + ), ], interfaces: vec![], structs: vec![], @@ -519,59 +448,30 @@ mod tests { errors: None, modules: vec![], }], - generators: None, - package: None, }) } #[test] - fn driver_walks_and_dispatches_in_canonical_order() { + fn driver_walks_dispatches_and_honors_prefix() { let dir = tempfile::tempdir().unwrap(); let out_dir = Utf8Path::from_path(dir.path()).unwrap(); run(&FakeBackend, &api(), out_dir, &FakeConfig::default()).unwrap(); let body = std::fs::read_to_string(out_dir.join("fake/out.txt")).unwrap(); assert_eq!( body, - "module math\nfn add [sync] weaveffi_math_add\nfn fetch [async] weaveffi_math_fetch\n" + "module math\nfn add [sync] weaveffi_math_add\nfn fetch [async] weaveffi_math_fetch\nfn scan [iter] weaveffi_math_scan\n" ); - } - #[test] - fn prefix_flows_into_symbols() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); let cfg = FakeConfig { prefix: Some("acme".into()), }; run(&FakeBackend, &api(), out_dir, &cfg).unwrap(); let body = std::fs::read_to_string(out_dir.join("fake/out.txt")).unwrap(); - assert!( - body.contains("acme_math_add"), - "prefix must reach symbols: {body}" - ); + assert!(body.contains("acme_math_add"), "{body}"); assert!(!body.contains("weaveffi_math_add")); - } - #[test] - fn output_files_are_sorted_paths() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); let files = output_files(&FakeBackend, &api(), out_dir, &FakeConfig::default()); assert_eq!(files.len(), 1); assert!(files[0].ends_with("fake/out.txt")); } - - // Exercise the generated Generator impl. - impl_generator_via_backend!(FakeBackend); - - #[test] - fn generator_bridge_delegates_to_driver() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let g = FakeBackend; - Generator::generate(&g, &api(), out_dir, &FakeConfig::default()).unwrap(); - assert!(out_dir.join("fake/out.txt").exists()); - let listed = Generator::output_files(&g, &api(), out_dir, &FakeConfig::default()); - assert_eq!(listed.len(), 1); - } } diff --git a/crates/weaveffi-core/src/cabi.rs b/crates/weaveffi-core/src/cabi.rs index 2d337d5c..2f7500ce 100644 --- a/crates/weaveffi-core/src/cabi.rs +++ b/crates/weaveffi-core/src/cabi.rs @@ -15,24 +15,55 @@ use std::fmt::Write; use crate::abi::{AbiParam, CType}; use crate::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle}; use crate::codegen::CodeWriter; +use crate::lang::{is_reserved, CPP_KEYWORDS, C_KEYWORDS}; use crate::model::{ AbiFn, CallShape, EnumBinding, ErrorBinding, FnBinding, InterfaceBinding, ModuleBinding, }; +/// The revision of the WeaveFFI C ABI the generators emit bindings for. +/// +/// Mirrors `weaveffi_abi::ABI_VERSION`; the two are kept equal by a test in +/// the `weaveffi` facade crate, which depends on both. Generated consumers +/// embed this value and, where a load-time check is cheap, compare it against +/// the producer's exported `{prefix}_abi_version()` before making any other +/// call. +pub const ABI_VERSION: u32 = 1; + /// Emit a `/** ... */` doc comment at `indent`. pub fn emit_doc(out: &mut String, doc: &Option, indent: &str) { common_emit_doc(out, doc, indent, DocCommentStyle::Javadoc); } /// 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 { params .iter() - .map(|p| format!("{} {}", p.ty.render_c(prefix), p.name)) + .map(|p| format!("{} {}", p.ty.render_c(prefix), c_param_name(&p.name))) .collect::>() .join(", ") } +/// The spelling of an IDL parameter name inside a C prototype. +/// +/// 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. +#[must_use] +pub fn c_param_name(name: &str) -> String { + if is_reserved(name, C_KEYWORDS) || is_reserved(name, CPP_KEYWORDS) { + format!("{name}_") + } else { + name.to_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 @@ -129,12 +160,19 @@ pub fn fn_decl(out: &mut String, f: &AbiFn, prefix: &str) { /// bytes, and arrays into linear memory before each call. Native consumers /// never call them, but a producer targeting WebAssembly (for example a C /// library built with Emscripten) must export them; the generated -/// `{prefix}.c` scaffold provides malloc/free-backed defaults. +/// `{prefix}.c` convenience file provides malloc/free-backed defaults. pub fn render_runtime_decls(out: &mut String, prefix: &str) { let api = export_macro(prefix); + let upper = prefix.to_uppercase(); let _ = write!( out, - "typedef uint64_t {prefix}_handle_t;\n\n\ + "/* The WeaveFFI C ABI revision this header was generated against. The\n \ + producer exports {prefix}_abi_version() so a consumer can refuse to load\n \ + a library built for a different revision instead of misreading its\n \ + error struct or value buffers. */\n\ + #define {upper}_ABI_VERSION {ABI_VERSION}u\n\ + {api} uint32_t {prefix}_abi_version(void);\n\n\ + typedef uint64_t {prefix}_handle_t;\n\n\ /* Error slot written by every fallible call. `payload_ptr`/`payload_len`\n \ hold the matched error code's fields serialized in the WeaveFFI value\n \ buffer format (null when the code declares no fields); both the message\n \ diff --git a/crates/weaveffi-core/src/cache.rs b/crates/weaveffi-core/src/cache.rs index cf3155ee..071c7bd4 100644 --- a/crates/weaveffi-core/src/cache.rs +++ b/crates/weaveffi-core/src/cache.rs @@ -1,10 +1,18 @@ -//! Content-hashing and per-generator caching for skip-if-unchanged builds. +//! Content-hashing and per-target caching for skip-if-unchanged builds. +//! +//! The orchestrator stores one hash per target under +//! `{out_dir}/.weaveffi-cache/{target}.hash`. The hash covers every input +//! that affects that target's output (the canonical IR, the target name, its +//! serialized config, and the CLI version), so a change to any of them +//! re-runs exactly the targets it affects. use anyhow::{Context, Result}; use camino::Utf8Path; use sha2::{Digest, Sha256}; use weaveffi_ir::ir::Api; +use crate::resolved::ResolvedApi; + const CACHE_DIR: &str = ".weaveffi-cache"; /// Version string baked into every cache entry. Bumping the WeaveFFI CLI @@ -12,141 +20,90 @@ const CACHE_DIR: &str = ".weaveffi-cache"; /// stale generator output after an upgrade. pub const CLI_VERSION: &str = env!("CARGO_PKG_VERSION"); -/// Serialize the API to canonical JSON and return its SHA-256 hex digest. -/// -/// The IR is first serialized to a `serde_json::Value`, whose `Object` -/// representation is backed by a `BTreeMap` (when the `preserve_order` -/// feature is not enabled). Re-serializing that `Value` therefore emits -/// keys in deterministic, lexicographic order regardless of the iteration -/// order of any source maps. This guarantees that two runs over the same -/// IR always produce the same hash. -/// -/// # Panics -/// -/// Panics if `api` cannot be serialized to JSON. This does not happen for a -/// well-formed [`Api`], whose IR is plain serializable data. -pub fn hash_api(api: &Api) -> String { +/// Serialize `api` to canonical JSON (keys in lexicographic order, so two +/// runs over the same IR always agree). +fn canonical_json(api: &Api) -> String { let value = serde_json::to_value(api).expect("Api serialization should not fail"); - let json = serde_json::to_string(&value).expect("Value serialization should not fail"); - let hash = Sha256::digest(json.as_bytes()); - format!("{hash:x}") + serde_json::to_string(&value).expect("Value serialization should not fail") } -/// Return the SHA-256 hex digest of the API content keyed by `generator_name`. -/// -/// Kept for tests and direct callers that only need an IR-keyed digest; -/// the orchestrator goes through [`hash_generator_inputs`] so that config -/// and CLI version changes invalidate the cache too. +/// The SHA-256 hex digest of the API's canonical JSON. /// /// # Panics /// -/// Panics if `api` cannot be serialized to JSON, which does not happen for a -/// well-formed [`Api`]. -pub fn hash_api_for_generator(api: &Api, generator_name: &str) -> String { - let value = serde_json::to_value(api).expect("Api serialization should not fail"); - let json = serde_json::to_string(&value).expect("Value serialization should not fail"); - let mut hasher = Sha256::new(); - hasher.update(generator_name.as_bytes()); - hasher.update(b":"); - hasher.update(json.as_bytes()); - let hash = hasher.finalize(); - format!("{hash:x}") +/// Panics if `api` cannot be serialized to JSON. This does not happen for a +/// well-formed [`Api`], whose IR is plain serializable data. +pub fn hash_api(api: &Api) -> String { + format!("{:x}", Sha256::digest(canonical_json(api).as_bytes())) } -/// Return the SHA-256 hex digest of every input that affects a single -/// generator's output: the canonical IR, the generator's name, the -/// generator's typed config (already serialized to canonical JSON bytes -/// by the caller via [`crate::codegen::DynGenerator::config_hash_input`]), +/// The SHA-256 hex digest of every input that affects a single target's +/// output: the canonical IR, the attached package identity, the target's +/// name, the target's serialized config (canonical JSON bytes from +/// [`Target::config_hash_input`](crate::codegen::Target::config_hash_input)), /// and the CLI version. /// -/// This is the cache key the orchestrator stores under -/// `{out_dir}/.weaveffi-cache/{generator_name}.hash`, so any change to -/// the IR, generator config, or CLI version invalidates that entry and -/// triggers a re-run. -/// /// # Panics /// -/// Panics if `api` cannot be serialized to JSON, which does not happen for a -/// well-formed [`Api`]. The `config_bytes` are already serialized by the -/// caller and are hashed as-is. -pub fn hash_generator_inputs(api: &Api, generator_name: &str, config_bytes: &[u8]) -> String { - let api_value = serde_json::to_value(api).expect("Api serialization should not fail"); - let api_json = serde_json::to_string(&api_value).expect("Value serialization should not fail"); - +/// Panics if the API or package cannot be serialized to JSON, which does not +/// happen for well-formed inputs. +pub fn hash_generator_inputs(api: &ResolvedApi, target: &str, config_bytes: &[u8]) -> String { + let package = + serde_json::to_string(&api.package()).expect("Package serialization should not fail"); let mut hasher = Sha256::new(); - hasher.update(b"v1\0"); + hasher.update(b"v2\0"); hasher.update(CLI_VERSION.as_bytes()); hasher.update(b"\0"); - hasher.update(generator_name.as_bytes()); + hasher.update(target.as_bytes()); + hasher.update(b"\0"); + hasher.update(canonical_json(api.api()).as_bytes()); hasher.update(b"\0"); - hasher.update(api_json.as_bytes()); + hasher.update(package.as_bytes()); hasher.update(b"\0"); hasher.update(config_bytes); - let hash = hasher.finalize(); - format!("{hash:x}") + format!("{:x}", hasher.finalize()) } -/// Read the persisted hash for `generator_name` from `out_dir/.weaveffi-cache/`. -/// -/// Returns `None` when no cache entry exists yet (or it is empty). -pub fn read_generator_cache(out_dir: &Utf8Path, generator_name: &str) -> Option { - let path = out_dir - .join(CACHE_DIR) - .join(format!("{generator_name}.hash")); - std::fs::read_to_string(path) +fn entry_path(out_dir: &Utf8Path, target: &str) -> camino::Utf8PathBuf { + out_dir.join(CACHE_DIR).join(format!("{target}.hash")) +} + +/// Read the persisted hash for `target`, or `None` when no non-empty entry +/// exists yet. +pub fn read_generator_cache(out_dir: &Utf8Path, target: &str) -> Option { + std::fs::read_to_string(entry_path(out_dir, target)) .ok() .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) } -/// Persist `hash` as the cache entry for `generator_name`. -/// -/// Removes a stale legacy `.weaveffi-cache` regular file (written by older -/// CLI versions that used a single global cache) before creating the new -/// per-generator directory layout. +/// Persist `hash` as the cache entry for `target`. /// /// # Errors /// -/// Returns an error if the legacy cache file cannot be removed, the cache -/// directory cannot be created, or the hash file cannot be written. -pub fn write_generator_cache(out_dir: &Utf8Path, generator_name: &str, hash: &str) -> Result<()> { - let cache_dir = out_dir.join(CACHE_DIR); - migrate_legacy_cache(out_dir)?; - std::fs::create_dir_all(cache_dir.as_std_path()) - .with_context(|| format!("failed to create cache directory: {cache_dir}"))?; - let path = cache_dir.join(format!("{generator_name}.hash")); +/// Returns an error if the cache directory cannot be created or the hash +/// file cannot be written. +pub fn write_generator_cache(out_dir: &Utf8Path, target: &str, hash: &str) -> Result<()> { + let dir = out_dir.join(CACHE_DIR); + let path = dir.join(format!("{target}.hash")); + std::fs::create_dir_all(dir.as_std_path()) + .with_context(|| format!("failed to create cache directory: {dir}"))?; std::fs::write(path.as_std_path(), hash) .with_context(|| format!("failed to write cache file: {path}"))?; Ok(()) } /// Delete every persisted cache entry under `out_dir/.weaveffi-cache/`. -/// /// Called when `--force` is used so subsequent runs always regenerate. /// /// # Errors /// -/// Returns an error if the cache directory (or a stale legacy cache file) -/// exists but cannot be removed. +/// Returns an error if the cache directory exists but cannot be removed. pub fn invalidate_all(out_dir: &Utf8Path) -> Result<()> { let cache_dir = out_dir.join(CACHE_DIR); - if cache_dir.is_dir() { + if cache_dir.exists() { std::fs::remove_dir_all(cache_dir.as_std_path()) .with_context(|| format!("failed to remove cache directory: {cache_dir}"))?; - } else if cache_dir.exists() { - std::fs::remove_file(cache_dir.as_std_path()) - .with_context(|| format!("failed to remove legacy cache file: {cache_dir}"))?; - } - Ok(()) -} - -/// Remove a stale legacy single-file cache so we can create the new -/// per-generator directory in its place. -fn migrate_legacy_cache(out_dir: &Utf8Path) -> Result<()> { - let cache_path = out_dir.join(CACHE_DIR); - if cache_path.is_file() { - std::fs::remove_file(cache_path.as_std_path()) - .with_context(|| format!("failed to remove legacy cache file: {cache_path}"))?; } Ok(()) } @@ -154,53 +111,15 @@ fn migrate_legacy_cache(out_dir: &Utf8Path) -> Result<()> { #[cfg(test)] mod tests { use super::*; - use crate::codegen::{ConfiguredGenerator, Generator, Orchestrator, OrchestratorHooks}; - use crate::resolved::ResolvedApi; - use std::sync::atomic::{AtomicUsize, Ordering}; - use std::sync::Arc; - use weaveffi_ir::ir::{Function, Module, Param, TypeRef}; - - /// Minimal serde-able config so the cache tests can exercise the - /// orchestrator without depending on any real per-language config. - #[derive(Default, Clone, serde::Serialize, serde::Deserialize)] - struct TestConfig { - knob: Option, - } - - fn config_bytes(c: &TestConfig) -> Vec { - let v = serde_json::to_value(c).unwrap(); - serde_json::to_vec(&v).unwrap() - } + use weaveffi_ir::ir::{Module, CURRENT_SCHEMA_VERSION}; - fn minimal_api() -> Api { + fn api(name: &str) -> Api { Api { - version: "0.7.0".to_string(), + version: CURRENT_SCHEMA_VERSION.into(), modules: vec![Module { - name: "math".to_string(), - functions: vec![Function { - name: "add".to_string(), - params: vec![ - Param { - name: "a".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], + name: name.into(), + doc: None, + functions: vec![], interfaces: vec![], structs: vec![], enums: vec![], @@ -209,539 +128,41 @@ mod tests { errors: None, modules: vec![], }], - generators: None, - package: None, - } - } - - struct CountingGenerator { - name: &'static str, - calls: Arc, - } - - impl Generator for CountingGenerator { - type Config = TestConfig; - - fn name(&self) -> &'static str { - self.name - } - - fn capabilities(&self) -> crate::capabilities::TargetCapabilities { - crate::capabilities::TargetCapabilities::full() - } - - fn generate( - &self, - _api: &crate::resolved::ResolvedApi, - out_dir: &Utf8Path, - _config: &Self::Config, - ) -> anyhow::Result<()> { - self.calls.fetch_add(1, Ordering::SeqCst); - let dir = out_dir.join(self.name); - std::fs::create_dir_all(dir.as_std_path())?; - std::fs::write(dir.join("output.txt").as_std_path(), "generated")?; - Ok(()) - } - } - - fn configured( - name: &'static str, - calls: Arc, - cfg: TestConfig, - ) -> ConfiguredGenerator { - ConfiguredGenerator::new(CountingGenerator { name, calls }, cfg) - } - - #[test] - fn hash_deterministic() { - let api = minimal_api(); - let h1 = hash_api(&api); - let h2 = hash_api(&api); - assert_eq!(h1, h2); - assert_eq!(h1.len(), 64); - } - - #[test] - fn hash_is_deterministic_across_runs() { - let mut api = minimal_api(); - let mut generators = std::collections::BTreeMap::new(); - let mut swift = toml::value::Table::new(); - swift.insert( - "module_name".into(), - toml::Value::String("MySwiftModule".into()), - ); - generators.insert("swift".into(), toml::Value::Table(swift)); - let mut android = toml::value::Table::new(); - android.insert( - "package".into(), - toml::Value::String("com.example.app".into()), - ); - generators.insert("android".into(), toml::Value::Table(android)); - api.generators = Some(generators); - - let baseline = hash_api(&api); - for _ in 0..100 { - assert_eq!( - hash_api(&api), - baseline, - "hash_api must produce identical output on every call" - ); } } #[test] - fn hash_changes_on_modification() { - let mut api = minimal_api(); - let h1 = hash_api(&api); - - api.modules[0].functions.push(Function { - name: "subtract".to_string(), - params: vec![ - Param { - name: "a".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }); - let h2 = hash_api(&api); - - assert_ne!(h1, h2); - } - - #[test] - fn per_generator_hash_includes_name() { - let api = minimal_api(); - let h_c = hash_api_for_generator(&api, "c"); - let h_swift = hash_api_for_generator(&api, "swift"); - assert_ne!(h_c, h_swift); - assert_eq!(h_c.len(), 64); - } - - #[test] - fn per_generator_hash_deterministic() { - let api = minimal_api(); - assert_eq!( - hash_api_for_generator(&api, "c"), - hash_api_for_generator(&api, "c"), - ); - } - - #[test] - fn per_generator_cache_round_trip() { - let dir = tempfile::tempdir().unwrap(); - let dir_path = Utf8Path::from_path(dir.path()).unwrap(); - - let hash = hash_api_for_generator(&minimal_api(), "c"); - write_generator_cache(dir_path, "c", &hash).unwrap(); - - let read_back = read_generator_cache(dir_path, "c"); - assert_eq!(read_back, Some(hash)); - assert_eq!(read_generator_cache(dir_path, "swift"), None); - } - - #[test] - fn read_generator_cache_returns_none_when_missing() { - let dir = tempfile::tempdir().unwrap(); - let dir_path = Utf8Path::from_path(dir.path()).unwrap(); - assert_eq!(read_generator_cache(dir_path, "c"), None); - } - - #[test] - fn invalidate_all_clears_cache() { - let dir = tempfile::tempdir().unwrap(); - let dir_path = Utf8Path::from_path(dir.path()).unwrap(); - write_generator_cache(dir_path, "c", "abc").unwrap(); - write_generator_cache(dir_path, "swift", "def").unwrap(); - - invalidate_all(dir_path).unwrap(); - assert_eq!(read_generator_cache(dir_path, "c"), None); - assert_eq!(read_generator_cache(dir_path, "swift"), None); - } - - #[test] - fn legacy_cache_file_is_replaced_by_directory() { - let dir = tempfile::tempdir().unwrap(); - let dir_path = Utf8Path::from_path(dir.path()).unwrap(); - std::fs::write(dir_path.join(CACHE_DIR), "stale-global-hash").unwrap(); - assert!(dir_path.join(CACHE_DIR).is_file()); - - write_generator_cache(dir_path, "c", "fresh-hash").unwrap(); - - assert!(dir_path.join(CACHE_DIR).is_dir()); - assert_eq!( - read_generator_cache(dir_path, "c"), - Some("fresh-hash".to_string()) - ); - } - - #[test] - fn cache_file_written_after_generate() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let api = minimal_api(); - let hooks = OrchestratorHooks::default(); - let calls = Arc::new(AtomicUsize::new(0)); - let gen = configured("counting", Arc::clone(&calls), TestConfig::default()); - - let orch = Orchestrator::new().with_generator(&gen); - orch.run( - &ResolvedApi::assume_resolved(api.clone()), - out_dir, - &hooks, - false, - ) - .unwrap(); - - assert!(out_dir.join(CACHE_DIR).join("counting.hash").exists()); - assert_eq!(calls.load(Ordering::SeqCst), 1); - } - - #[test] - fn cache_prevents_regeneration() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let api = minimal_api(); - let hooks = OrchestratorHooks::default(); - let calls = Arc::new(AtomicUsize::new(0)); - let gen = configured("counting", Arc::clone(&calls), TestConfig::default()); - - let orch = Orchestrator::new().with_generator(&gen); - orch.run( - &ResolvedApi::assume_resolved(api.clone()), - out_dir, - &hooks, - false, - ) - .unwrap(); - assert_eq!(calls.load(Ordering::SeqCst), 1); - - orch.run( - &ResolvedApi::assume_resolved(api.clone()), - out_dir, - &hooks, - false, - ) - .unwrap(); - assert_eq!( - calls.load(Ordering::SeqCst), - 1, - "second run should skip generation" - ); - } - - #[test] - fn cache_invalidated_on_api_change() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let api = minimal_api(); - let hooks = OrchestratorHooks::default(); - let calls = Arc::new(AtomicUsize::new(0)); - let gen = configured("counting", Arc::clone(&calls), TestConfig::default()); - - let orch = Orchestrator::new().with_generator(&gen); - orch.run( - &ResolvedApi::assume_resolved(api.clone()), - out_dir, - &hooks, - false, - ) - .unwrap(); - assert_eq!(calls.load(Ordering::SeqCst), 1); - - let mut modified_api = api; - modified_api.modules[0].functions.push(Function { - name: "subtract".to_string(), - params: vec![ - Param { - name: "a".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }); - - orch.run( - &ResolvedApi::assume_resolved(modified_api.clone()), - out_dir, - &hooks, - false, - ) - .unwrap(); - assert_eq!( - calls.load(Ordering::SeqCst), - 2, - "changed API should trigger regeneration" - ); - } - - #[test] - fn force_flag_bypasses_cache() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let api = minimal_api(); - let hooks = OrchestratorHooks::default(); - let calls = Arc::new(AtomicUsize::new(0)); - let gen = configured("counting", Arc::clone(&calls), TestConfig::default()); - - let orch = Orchestrator::new().with_generator(&gen); - orch.run( - &ResolvedApi::assume_resolved(api.clone()), - out_dir, - &hooks, - true, - ) - .unwrap(); - assert_eq!(calls.load(Ordering::SeqCst), 1); - - orch.run( - &ResolvedApi::assume_resolved(api.clone()), - out_dir, - &hooks, - true, - ) - .unwrap(); - assert_eq!( - calls.load(Ordering::SeqCst), - 2, - "force=true should bypass cache" - ); - } - - #[test] - fn legacy_cache_file_ignored_on_first_run() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - std::fs::write(out_dir.join(CACHE_DIR), "stale-legacy").unwrap(); - - let api = minimal_api(); - let hooks = OrchestratorHooks::default(); - let calls = Arc::new(AtomicUsize::new(0)); - let gen = configured("counting", Arc::clone(&calls), TestConfig::default()); - - let orch = Orchestrator::new().with_generator(&gen); - orch.run( - &ResolvedApi::assume_resolved(api.clone()), - out_dir, - &hooks, - false, - ) - .unwrap(); - assert_eq!( - calls.load(Ordering::SeqCst), - 1, - "legacy single-file cache must not skip first run" - ); - assert!(out_dir.join(CACHE_DIR).is_dir()); - } - - #[test] - fn single_generator_cache_invalidates_independently() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let hooks = OrchestratorHooks::default(); - let c_calls = Arc::new(AtomicUsize::new(0)); - let s_calls = Arc::new(AtomicUsize::new(0)); - let c_gen = configured("c", Arc::clone(&c_calls), TestConfig::default()); - let s_gen = configured("swift", Arc::clone(&s_calls), TestConfig::default()); - let orch = Orchestrator::new() - .with_generator(&c_gen) - .with_generator(&s_gen); - - let api = minimal_api(); - orch.run( - &ResolvedApi::assume_resolved(api.clone()), - out_dir, - &hooks, - false, - ) - .unwrap(); - assert_eq!(c_calls.load(Ordering::SeqCst), 1); - assert_eq!(s_calls.load(Ordering::SeqCst), 1); - - // Invalidate only the C generator's cache; the API itself is unchanged. - std::fs::remove_file(out_dir.join(CACHE_DIR).join("c.hash")).unwrap(); - - orch.run( - &ResolvedApi::assume_resolved(api.clone()), - out_dir, - &hooks, - false, - ) - .unwrap(); - assert_eq!( - c_calls.load(Ordering::SeqCst), - 2, - "C generator should re-run after its cache entry was removed" - ); - assert_eq!( - s_calls.load(Ordering::SeqCst), - 1, - "Swift generator's cache is intact and must be skipped" - ); - } - - #[test] - fn hash_generator_inputs_changes_when_config_bytes_change() { - let api = minimal_api(); - let base = config_bytes(&TestConfig::default()); - - let changed = config_bytes(&TestConfig { - knob: Some("flipped".into()), - }); - + fn hashes_are_stable_and_keyed_on_every_input() { + let a = api("math"); + assert_eq!(hash_api(&a), hash_api(&a.clone())); + assert_ne!(hash_api(&a), hash_api(&api("math2"))); + let r = ResolvedApi::assume_valid(a); + let base = hash_generator_inputs(&r, "swift", b"{}"); + assert_eq!(base, hash_generator_inputs(&r, "swift", b"{}")); + assert_ne!(base, hash_generator_inputs(&r, "c", b"{}")); + assert_ne!(base, hash_generator_inputs(&r, "swift", b"{\"x\":1}")); assert_ne!( - hash_generator_inputs(&api, "c", &base), - hash_generator_inputs(&api, "c", &changed), - "changing config bytes must change the per-generator hash" - ); - } - - #[test] - fn hash_generator_inputs_includes_cli_version() { - let api = minimal_api(); - let cfg = config_bytes(&TestConfig::default()); - - // Compute the canonical hash, then compute the digest the same way - // but pretend a different CLI version produced it. The two must - // differ; otherwise upgrades silently leave stale output. - let real = hash_generator_inputs(&api, "c", &cfg); - - let api_value = serde_json::to_value(&api).unwrap(); - let api_json = serde_json::to_string(&api_value).unwrap(); - - let mut h = Sha256::new(); - h.update(b"v1\0"); - h.update(b"0.0.0-pretend-old\0"); - h.update(b"c\0"); - h.update(api_json.as_bytes()); - h.update(b"\0"); - h.update(&cfg); - let pretend = format!("{:x}", h.finalize()); - - assert_ne!( - real, pretend, - "CLI_VERSION must be part of the cache key so an upgrade invalidates it" - ); - assert_eq!(CLI_VERSION, env!("CARGO_PKG_VERSION")); - } - - #[test] - fn cache_invalidated_on_config_only_change() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let api = minimal_api(); - let hooks = OrchestratorHooks::default(); - - let calls = Arc::new(AtomicUsize::new(0)); - let gen = configured("c", Arc::clone(&calls), TestConfig::default()); - Orchestrator::new() - .with_generator(&gen) - .run( - &ResolvedApi::assume_resolved(api.clone()), - out_dir, - &hooks, - false, - ) - .unwrap(); - assert_eq!(calls.load(Ordering::SeqCst), 1); - - // Re-run with the *same* IR but a changed generator config. - let gen2 = configured( - "c", - Arc::clone(&calls), - TestConfig { - knob: Some("changed".into()), - }, - ); - Orchestrator::new() - .with_generator(&gen2) - .run( - &ResolvedApi::assume_resolved(api.clone()), - out_dir, - &hooks, - false, - ) - .unwrap(); - assert_eq!( - calls.load(Ordering::SeqCst), - 2, - "changing generator config must invalidate the cache and re-run the generator" - ); - - // A third run with the same `changed` config should hit the cache again. - Orchestrator::new() - .with_generator(&gen2) - .run( - &ResolvedApi::assume_resolved(api.clone()), - out_dir, - &hooks, - false, - ) - .unwrap(); - assert_eq!( - calls.load(Ordering::SeqCst), - 2, - "running with the same config twice should not regenerate" + base, + hash_generator_inputs(&ResolvedApi::assume_valid(api("other")), "swift", b"{}") ); + let with_pkg = r.clone().with_package(crate::pkg::Package { + name: Some("kv".into()), + ..Default::default() + }); + assert_ne!(base, hash_generator_inputs(&with_pkg, "swift", b"{}")); } #[test] - fn cache_invalidated_when_pre_generated_hash_has_wrong_version() { + fn entries_round_trip_and_invalidate() { let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let api = minimal_api(); - let hooks = OrchestratorHooks::default(); - let calls = Arc::new(AtomicUsize::new(0)); - let gen = configured("c", Arc::clone(&calls), TestConfig::default()); - let orch = Orchestrator::new().with_generator(&gen); - - // Pre-seed the cache with a hash that was computed with the - // legacy IR-only function. The orchestrator now keys on - // `hash_generator_inputs`, so the stale entry must not match - // and the generator must re-run. - let stale = hash_api_for_generator(&api, "c"); - write_generator_cache(out_dir, "c", &stale).unwrap(); - - orch.run( - &ResolvedApi::assume_resolved(api.clone()), - out_dir, - &hooks, - false, - ) - .unwrap(); - assert_eq!( - calls.load(Ordering::SeqCst), - 1, - "legacy IR-only hash must not satisfy the new cache key shape" - ); + let out = Utf8Path::from_path(dir.path()).unwrap(); + assert_eq!(read_generator_cache(out, "swift"), None); + write_generator_cache(out, "swift", "abc").unwrap(); + write_generator_cache(out, "c", "def").unwrap(); + assert_eq!(read_generator_cache(out, "swift").as_deref(), Some("abc")); + assert_eq!(read_generator_cache(out, "c").as_deref(), Some("def")); + invalidate_all(out).unwrap(); + assert_eq!(read_generator_cache(out, "swift"), None); + invalidate_all(out).unwrap(); } } diff --git a/crates/weaveffi-core/src/capabilities.rs b/crates/weaveffi-core/src/capabilities.rs index d32c6eec..2486fcd4 100644 --- a/crates/weaveffi-core/src/capabilities.rs +++ b/crates/weaveffi-core/src/capabilities.rs @@ -61,9 +61,8 @@ impl fmt::Display for Feature { } } -/// The feature set a generator implements. Declared by every backend via -/// [`LanguageBackend::capabilities`](crate::backend::LanguageBackend::capabilities) -/// / [`Generator::capabilities`](crate::codegen::Generator::capabilities). +/// The feature set a backend implements. Declared by every backend via +/// [`LanguageBackend::capabilities`](crate::backend::LanguageBackend::capabilities). /// /// There is intentionally no `Default` impl: a backend must state what it /// supports explicitly so a new gated feature cannot be claimed by omission. @@ -128,8 +127,20 @@ fn collect_module(module: &Module, parent: &str, used: &mut BTreeMap Module { Module { name: name.into(), + doc: None, functions: vec![], interfaces: vec![], structs: vec![], @@ -243,10 +256,8 @@ mod tests { fn api(modules: Vec) -> Api { Api { - version: "0.7.0".into(), + version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), modules, - generators: None, - package: None, } } diff --git a/crates/weaveffi-core/src/codegen/common.rs b/crates/weaveffi-core/src/codegen/common.rs index 8b5f79e9..0f144c0e 100644 --- a/crates/weaveffi-core/src/codegen/common.rs +++ b/crates/weaveffi-core/src/codegen/common.rs @@ -155,6 +155,7 @@ mod tests { fn leaf(name: &str) -> Module { Module { name: name.to_string(), + doc: None, functions: vec![], interfaces: vec![], structs: vec![], diff --git a/crates/weaveffi-core/src/codegen/mod.rs b/crates/weaveffi-core/src/codegen/mod.rs index 62cdf16a..6fd5bbb6 100644 --- a/crates/weaveffi-core/src/codegen/mod.rs +++ b/crates/weaveffi-core/src/codegen/mod.rs @@ -1,17 +1,16 @@ -//! Generator trait, dyn-erasure wrapper, and orchestration. +//! Target erasure and orchestration. //! -//! Each language target implements [`Generator`] with its own associated -//! `Config` type. The orchestrator works on the object-safe [`DynGenerator`] -//! trait, which erases the concrete config and is what tests and the CLI -//! pass into [`Orchestrator::with_generator`]. The recommended way to -//! produce a `&dyn DynGenerator` is to build a [`ConfiguredGenerator`] -//! that pairs a typed generator with its concrete config value. +//! 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`]. use anyhow::{bail, Result}; use camino::Utf8Path; use rayon::prelude::*; -use serde::Serialize; +use crate::backend::{self, LanguageBackend}; use crate::cache; use crate::capabilities::{self, TargetCapabilities}; use crate::package::{PackageContext, PackagedFile}; @@ -39,110 +38,30 @@ fn run_hook(label: &str, cmd: &str) -> Result<()> { Ok(()) } -/// A language code generator. +/// Object-safe view of a [`LanguageBackend`] paired with a concrete config. /// -/// Generators are dispatched in parallel, so every implementation must be -/// safe to share across threads. The associated [`Config`] type is owned -/// by the generator crate so `weaveffi-core` does not have to know about -/// target-specific options like `swift_module_name` or `cpp_namespace`. -/// -/// [`Config`]: Generator::Config -pub trait Generator: Send + Sync { - /// Per-target, fully-typed configuration consumed by [`generate`] and - /// [`output_files`]. Must round-trip through `serde_json` so the - /// orchestrator can hash it as part of the cache key. - /// - /// [`generate`]: Generator::generate - /// [`output_files`]: Generator::output_files - type Config: Serialize + Default + Clone + Send + Sync; - - /// Stable short name for the target (`"swift"`, `"c"`, `"node"`, …). - /// Used as the cache file basename and the `--target` filter token. - fn name(&self) -> &'static str; - - /// The gated IDL features this target implements. The orchestrator - /// refuses to run a generator against an API that uses a feature its - /// declared capabilities do not cover: a target either generates a - /// feature or fails loudly; it never silently omits one. - fn capabilities(&self) -> TargetCapabilities; - - /// Whether the user explicitly opted in to generating this target even - /// though the API uses features the target does not support (via an - /// `allow_unsupported: true` flag in the target's config). When `true` the - /// orchestrator downgrades the capability failure to a loud warning and - /// the generator must emit an explicit unsupported surface (throwing - /// stubs, documentation) rather than silently omitting the feature. - /// Default: `false`. Opting in must always be an explicit config act. - fn allows_unsupported(&self, config: &Self::Config) -> bool { - let _ = config; - false - } - - /// Render the bindings under `out_dir`. - /// - /// # Errors - /// - /// Returns an error if the target cannot render the API or cannot write its - /// output files (for example a filesystem failure). - fn generate(&self, api: &ResolvedApi, out_dir: &Utf8Path, config: &Self::Config) -> Result<()>; - - /// Files that [`generate`](Generator::generate) would write, relative - /// to (or anchored under) `out_dir`. Used by `--dry-run` and `diff`. - /// Default implementation returns the empty list; generators override - /// to surface the list without doing any I/O. - fn output_files( - &self, - _api: &ResolvedApi, - _out_dir: &Utf8Path, - _config: &Self::Config, - ) -> Vec { - vec![] - } - - /// Assemble a distributable package that bundles the prebuilt native - /// libraries in `ctx`, anchored under `out_dir`, or `None` when this target - /// does not support packaging. Mirrors - /// [`LanguageBackend::package`](crate::backend::LanguageBackend::package); - /// the default returns `None`. - fn package( - &self, - _api: &ResolvedApi, - _ctx: &PackageContext, - _out_dir: &Utf8Path, - _config: &Self::Config, - ) -> Option> { - None - } -} - -/// Object-safe view of a [`Generator`] paired with a concrete config. -/// -/// The orchestrator stores generators as `&dyn DynGenerator` so it can -/// hold a heterogeneous set of targets whose `Config` types differ. -/// [`ConfiguredGenerator`] is the canonical adapter. -pub trait DynGenerator: Send + Sync { - /// The target's stable short name. Mirrors [`Generator::name`]. +/// 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; /// The gated features the target implements. Mirrors - /// [`Generator::capabilities`]. + /// [`LanguageBackend::capabilities`]. fn capabilities(&self) -> TargetCapabilities; - /// See [`Generator::allows_unsupported`], evaluated against the bound - /// config. + /// See [`LanguageBackend::allows_unsupported`], evaluated against the + /// bound config. fn allows_unsupported(&self) -> bool; - /// Render the bindings under `out_dir`, using the bound config. Mirrors - /// [`Generator::generate`]. + /// Render the bindings under `out_dir`, using the bound config. /// /// # Errors /// - /// Returns an error if the underlying [`Generator::generate`] fails to - /// render or write its output. + /// Returns an error if the backend fails to render or write its output. fn generate(&self, api: &ResolvedApi, out_dir: &Utf8Path) -> Result<()>; - /// The files [`generate`](Self::generate) would write. Mirrors - /// [`Generator::output_files`]. + /// The files [`generate`](Self::generate) would write. fn output_files(&self, api: &ResolvedApi, out_dir: &Utf8Path) -> Vec; /// Assemble the distributable package for this target, using the bound - /// config. Mirrors [`Generator::package`]; returns `None` when the target - /// does not support packaging. + /// config. Returns `None` when the target does not support packaging. fn package( &self, api: &ResolvedApi, @@ -154,36 +73,36 @@ pub trait DynGenerator: Send + Sync { fn config_hash_input(&self) -> Vec; } -/// Binds a [`Generator`] to a concrete [`Generator::Config`] value so it -/// can be erased to `&dyn DynGenerator`. +/// Binds a [`LanguageBackend`] to a concrete config value so it can be erased +/// to `&dyn Target`. /// /// ```ignore -/// let swift = ConfiguredGenerator::new(SwiftGenerator, SwiftConfig::default()); -/// orchestrator.with_generator(&swift); +/// let swift = ConfiguredBackend::new(SwiftBackend, SwiftConfig::default()); +/// orchestrator.with_target(&swift); /// ``` -pub struct ConfiguredGenerator { - inner: G, - config: G::Config, +pub struct ConfiguredBackend { + inner: B, + config: B::Config, } -impl ConfiguredGenerator { - /// Pair a typed generator with the concrete config it should run under. - pub fn new(inner: G, config: G::Config) -> Self { +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 } } /// Borrow the bound config value. - pub fn config(&self) -> &G::Config { + pub fn config(&self) -> &B::Config { &self.config } - /// Borrow the wrapped typed generator. - pub fn inner(&self) -> &G { + /// Borrow the wrapped backend. + pub fn inner(&self) -> &B { &self.inner } } -impl DynGenerator for ConfiguredGenerator { +impl Target for ConfiguredBackend { fn name(&self) -> &'static str { self.inner.name() } @@ -197,11 +116,11 @@ impl DynGenerator for ConfiguredGenerator { } fn generate(&self, api: &ResolvedApi, out_dir: &Utf8Path) -> Result<()> { - self.inner.generate(api, out_dir, &self.config) + backend::run(&self.inner, api, out_dir, &self.config) } fn output_files(&self, api: &ResolvedApi, out_dir: &Utf8Path) -> Vec { - self.inner.output_files(api, out_dir, &self.config) + backend::output_files(&self.inner, api, out_dir, &self.config) } fn package( @@ -210,12 +129,12 @@ impl DynGenerator for ConfiguredGenerator { ctx: &PackageContext, out_dir: &Utf8Path, ) -> Option> { - self.inner.package(api, ctx, out_dir, &self.config) + backend::package_files(&self.inner, api, ctx, out_dir, &self.config) } fn config_hash_input(&self) -> Vec { let value = - serde_json::to_value(&self.config).expect("generator config should serialize to JSON"); + serde_json::to_value(&self.config).expect("backend config should serialize to JSON"); serde_json::to_vec(&value).expect("JSON Value should serialize") } } @@ -230,12 +149,11 @@ pub struct OrchestratorHooks { pub post_generate: Option, } -/// Runs a set of configured generators: it capability-gates each target, -/// skips the ones whose cached hash is still current, and renders the rest in -/// parallel. +/// Runs a set of configured targets: it capability-gates each one, skips the +/// ones whose cached hash is still current, and renders the rest in parallel. #[derive(Default)] pub struct Orchestrator<'a> { - generators: Vec<&'a dyn DynGenerator>, + targets: Vec<&'a dyn Target>, } impl<'a> Orchestrator<'a> { @@ -244,9 +162,9 @@ impl<'a> Orchestrator<'a> { Self::default() } - /// Register one erased generator to run, returning `self` for chaining. - pub fn with_generator(mut self, gen: &'a dyn DynGenerator) -> Self { - self.generators.push(gen); + /// Register one erased target to run, returning `self` for chaining. + pub fn with_target(mut self, target: &'a dyn Target) -> Self { + self.targets.push(target); self } @@ -261,8 +179,8 @@ impl<'a> Orchestrator<'a> { /// /// Returns an error if a selected target does not support a feature the IDL /// uses (without `allow_unsupported`), the `force` cache reset fails, a - /// `pre_generate` or `post_generate` hook exits non-zero, any generator - /// fails while rendering, or a cache entry cannot be written. + /// `pre_generate` or `post_generate` hook exits non-zero, any target fails + /// while rendering, or a cache entry cannot be written. pub fn run( &self, api: &ResolvedApi, @@ -272,20 +190,20 @@ impl<'a> Orchestrator<'a> { ) -> Result<()> { // Capability gate: every selected target must support every gated // feature the API uses. Collect all violations before failing so the - // user sees the complete picture in one run. A generator whose config + // user sees the complete picture in one run. A target whose config // explicitly opted in via `allow_unsupported` downgrades its failure - // to a loud warning: the generator emits an explicit unsupported + // to a loud warning: the backend emits an explicit unsupported // surface (throwing stubs) for the missing features instead. let mut violations: Vec = Vec::new(); - for g in &self.generators { - let Err(err) = capabilities::check(api.api(), g.name(), &g.capabilities()) else { + for t in &self.targets { + let Err(err) = capabilities::check(api.api(), t.name(), &t.capabilities()) else { continue; }; - if g.allows_unsupported() { + if t.allows_unsupported() { eprintln!( "warning: target '{}' does not support every feature this IDL uses; \ generating anyway because allow_unsupported is set:", - g.name() + t.name() ); for (feature, locations) in &err.violations { eprintln!(" - {feature} (used by: {})", locations.join(", ")); @@ -302,16 +220,14 @@ impl<'a> Orchestrator<'a> { cache::invalidate_all(out_dir)?; } - // Pair each generator with its expected hash and decide individually - // whether it needs to run, so a single generator can be re-run while + // Pair each target with its expected hash and decide individually + // whether it needs to run, so a single target can be re-run while // the others stay cached. - let mut pending: Vec<(&'a dyn DynGenerator, String)> = Vec::new(); - for &g in &self.generators { - let cfg_bytes = g.config_hash_input(); - let hash = cache::hash_generator_inputs(api.api(), g.name(), &cfg_bytes); - let cached = cache::read_generator_cache(out_dir, g.name()); - if cached.as_deref() != Some(hash.as_str()) { - pending.push((g, hash)); + let mut pending: Vec<(&'a dyn Target, String)> = Vec::new(); + for &t in &self.targets { + let hash = cache::hash_generator_inputs(api, t.name(), &t.config_hash_input()); + if cache::read_generator_cache(out_dir, t.name()).as_deref() != Some(hash.as_str()) { + pending.push((t, hash)); } } @@ -326,15 +242,15 @@ impl<'a> Orchestrator<'a> { pending .par_iter() - .map(|(g, _)| g.generate(api, out_dir)) + .map(|(t, _)| t.generate(api, out_dir)) .collect::>>()?; if let Some(cmd) = &hooks.post_generate { run_hook("post_generate", cmd)?; } - for (g, hash) in &pending { - cache::write_generator_cache(out_dir, g.name(), hash)?; + for (t, hash) in &pending { + cache::write_generator_cache(out_dir, t.name(), hash)?; } Ok(()) } @@ -343,25 +259,25 @@ impl<'a> Orchestrator<'a> { #[cfg(test)] mod tests { use super::*; + use crate::backend::OutputFile; + use crate::model::BindingModel; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; - use weaveffi_ir::ir::{Api, Function, Module, Param, TypeRef}; + use weaveffi_ir::ir::{Api, CallbackDef, ListenerDef, Module}; - /// Test generator with a minimal config so tests don't have to depend - /// on any real per-language generator crate. - #[derive(Default, Clone, serde::Serialize, serde::Deserialize)] + #[derive(Default, Clone, serde::Serialize)] struct TestConfig { knob: Option, allow_unsupported: bool, } - struct CountingGenerator { + struct Counting { name: &'static str, calls: Arc, caps: TargetCapabilities, } - impl Generator for CountingGenerator { + impl LanguageBackend for Counting { type Config = TestConfig; fn name(&self) -> &'static str { @@ -376,103 +292,84 @@ mod tests { config.allow_unsupported } - fn generate( + fn files( &self, _api: &ResolvedApi, + _model: &BindingModel, out_dir: &Utf8Path, _config: &Self::Config, - ) -> Result<()> { + ) -> Vec { self.calls.fetch_add(1, Ordering::SeqCst); - let dir = out_dir.join(self.name); - std::fs::create_dir_all(dir.as_std_path())?; - std::fs::write(dir.join("output.txt").as_std_path(), "generated")?; - Ok(()) + vec![OutputFile::new( + out_dir.join(self.name).join("output.txt"), + "generated", + )] } } - fn test_api() -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.to_string(), - modules: vec![Module { - name: "math".to_string(), - functions: vec![Function { - name: "add".to_string(), - params: vec![ - Param { - name: "a".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }) + fn module(name: &str) -> Module { + Module { + name: name.into(), + doc: None, + functions: vec![], + interfaces: vec![], + structs: vec![], + enums: vec![], + callbacks: vec![], + listeners: vec![], + errors: None, + modules: vec![], + } } - fn configured( - name: &'static str, - calls: Arc, - ) -> ConfiguredGenerator { - ConfiguredGenerator::new( - CountingGenerator { - name, - calls, - caps: TargetCapabilities::full(), - }, - TestConfig::default(), - ) + fn api() -> ResolvedApi { + ResolvedApi::assume_valid(Api { + version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), + modules: vec![module("math")], + }) } /// An API that uses listeners, so a target without listener support /// trips the capability gate. fn listener_api() -> ResolvedApi { - let mut api = test_api().api().clone(); - api.modules[0].listeners = vec![weaveffi_ir::ir::ListenerDef { - name: "on_change".to_string(), - event_callback: "OnChange".to_string(), + let mut m = module("math"); + m.callbacks = vec![CallbackDef { + name: "OnChange".into(), + params: vec![], doc: None, }]; - api.modules[0].callbacks = vec![weaveffi_ir::ir::CallbackDef { - name: "OnChange".to_string(), - params: vec![], + m.listeners = vec![ListenerDef { + name: "on_change".into(), + event_callback: "OnChange".into(), doc: None, }]; - ResolvedApi::assume_resolved(api) + ResolvedApi::assume_valid(Api { + version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), + modules: vec![m], + }) + } + + fn full(name: &'static str, calls: &Arc) -> ConfiguredBackend { + ConfiguredBackend::new( + Counting { + name, + calls: Arc::clone(calls), + caps: TargetCapabilities::full(), + }, + TestConfig::default(), + ) } fn partial( - calls: Arc, + name: &'static str, + calls: &Arc, allow_unsupported: bool, - ) -> ConfiguredGenerator { - ConfiguredGenerator::new( - CountingGenerator { - name: "partial", - calls, + ) -> ConfiguredBackend { + ConfiguredBackend::new( + Counting { + name, + calls: Arc::clone(calls), caps: TargetCapabilities { - callbacks: false, listeners: false, ..TargetCapabilities::full() }, @@ -484,261 +381,110 @@ mod tests { ) } - #[test] - fn capability_gate_blocks_unsupported_target() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let calls = Arc::new(AtomicUsize::new(0)); - let gen = partial(Arc::clone(&calls), false); - - let err = Orchestrator::new() - .with_generator(&gen) - .run( - &listener_api(), - out_dir, - &OrchestratorHooks::default(), - false, - ) - .unwrap_err(); - - let msg = err.to_string(); - assert!(msg.contains("target 'partial' does not support"), "{msg}"); - assert!(msg.contains("math.on_change"), "{msg}"); - assert!(msg.contains("allow_unsupported"), "{msg}"); - assert_eq!( - calls.load(Ordering::SeqCst), - 0, - "gated generator must not run" - ); + fn out_dir(dir: &tempfile::TempDir) -> &Utf8Path { + Utf8Path::from_path(dir.path()).unwrap() } #[test] - fn allow_unsupported_downgrades_gate_to_warning() { + fn capability_gate_blocks_unless_opted_in_and_never_relaxes_others() { let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let calls = Arc::new(AtomicUsize::new(0)); - let gen = partial(Arc::clone(&calls), true); - - Orchestrator::new() - .with_generator(&gen) - .run( - &listener_api(), - out_dir, - &OrchestratorHooks::default(), - false, - ) - .expect("allow_unsupported must let generation proceed"); - - assert_eq!(calls.load(Ordering::SeqCst), 1, "generator should run"); - } - - #[test] - fn allow_unsupported_does_not_relax_other_targets() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let opted_calls = Arc::new(AtomicUsize::new(0)); + let hooks = OrchestratorHooks::default(); let strict_calls = Arc::new(AtomicUsize::new(0)); - let opted = partial(Arc::clone(&opted_calls), true); - let strict = ConfiguredGenerator::new( - CountingGenerator { - name: "strict", - calls: Arc::clone(&strict_calls), - caps: TargetCapabilities { - listeners: false, - ..TargetCapabilities::full() - }, - }, - TestConfig::default(), - ); + let opted_calls = Arc::new(AtomicUsize::new(0)); + let strict = partial("strict", &strict_calls, false); + let opted = partial("opted", &opted_calls, true); let err = Orchestrator::new() - .with_generator(&opted) - .with_generator(&strict) - .run( - &listener_api(), - out_dir, - &OrchestratorHooks::default(), - false, - ) - .unwrap_err(); - - let msg = err.to_string(); - assert!(msg.contains("target 'strict'"), "{msg}"); - assert!(!msg.contains("target 'partial'"), "{msg}"); - assert_eq!(opted_calls.load(Ordering::SeqCst), 0); + .with_target(&strict) + .with_target(&opted) + .run(&listener_api(), out_dir(&dir), &hooks, false) + .unwrap_err() + .to_string(); + assert!(err.contains("target 'strict' does not support"), "{err}"); + assert!(err.contains("math.on_change"), "{err}"); + assert!(!err.contains("target 'opted'"), "{err}"); assert_eq!(strict_calls.load(Ordering::SeqCst), 0); - } - - #[test] - fn incremental_skips_when_unchanged() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let api = test_api(); - let hooks = OrchestratorHooks::default(); - let calls = Arc::new(AtomicUsize::new(0)); - let gen = configured("counting", Arc::clone(&calls)); - - let orch = Orchestrator::new().with_generator(&gen); - - orch.run(&api, out_dir, &hooks, false).unwrap(); - assert_eq!(calls.load(Ordering::SeqCst), 1); - let content_after_first = - std::fs::read_to_string(out_dir.join("counting/output.txt")).unwrap(); - - orch.run(&api, out_dir, &hooks, false).unwrap(); - assert_eq!( - calls.load(Ordering::SeqCst), - 1, - "generator should not run again" - ); - let content_after_second = - std::fs::read_to_string(out_dir.join("counting/output.txt")).unwrap(); + assert_eq!(opted_calls.load(Ordering::SeqCst), 0); - assert_eq!(content_after_first, content_after_second); + Orchestrator::new() + .with_target(&opted) + .run(&listener_api(), out_dir(&dir), &hooks, false) + .expect("allow_unsupported must let generation proceed"); + assert_eq!(opted_calls.load(Ordering::SeqCst), 1); } #[test] - fn force_bypasses_cache() { + fn cache_skips_unchanged_and_invalidates_per_target() { let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let api = test_api(); + let out = out_dir(&dir); let hooks = OrchestratorHooks::default(); - let calls = Arc::new(AtomicUsize::new(0)); - let gen = configured("counting", Arc::clone(&calls)); - - let orch = Orchestrator::new().with_generator(&gen); - - orch.run(&api, out_dir, &hooks, false).unwrap(); - assert_eq!(calls.load(Ordering::SeqCst), 1); - - orch.run(&api, out_dir, &hooks, true).unwrap(); - assert_eq!(calls.load(Ordering::SeqCst), 2, "force should bypass cache"); + let c_calls = Arc::new(AtomicUsize::new(0)); + let s_calls = Arc::new(AtomicUsize::new(0)); + let c = full("c", &c_calls); + let s = full("swift", &s_calls); + let orch = Orchestrator::new().with_target(&c).with_target(&s); + + orch.run(&api(), out, &hooks, false).unwrap(); + orch.run(&api(), out, &hooks, false).unwrap(); + assert_eq!(c_calls.load(Ordering::SeqCst), 1, "unchanged inputs skip"); + assert!(out.join("c/output.txt").exists()); + + orch.run(&api(), out, &hooks, true).unwrap(); + assert_eq!(c_calls.load(Ordering::SeqCst), 2, "force bypasses cache"); + + // Change the API, but pre-seed Swift's entry with the new hash so only + // C is stale. + let modified = ResolvedApi::assume_valid(Api { + version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), + modules: vec![module("math2")], + }); + let swift_hash = cache::hash_generator_inputs(&modified, "swift", &s.config_hash_input()); + cache::write_generator_cache(out, "swift", &swift_hash).unwrap(); + orch.run(&modified, out, &hooks, false).unwrap(); + assert_eq!(c_calls.load(Ordering::SeqCst), 3); + assert_eq!(s_calls.load(Ordering::SeqCst), 2); + + // A config-only change re-runs. + let reconfigured = ConfiguredBackend::new( + Counting { + name: "c", + calls: Arc::clone(&c_calls), + caps: TargetCapabilities::full(), + }, + TestConfig { + knob: Some("changed".into()), + allow_unsupported: false, + }, + ); + Orchestrator::new() + .with_target(&reconfigured) + .run(&modified, out, &hooks, false) + .unwrap(); + assert_eq!(c_calls.load(Ordering::SeqCst), 4); } #[test] - fn parallel_orchestrator_runs_all_generators() { + fn all_targets_run_in_parallel() { let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let api = test_api(); - let hooks = OrchestratorHooks::default(); - let names = ["g0", "g1", "g2", "g3", "g4", "g5"]; let counters: Vec> = names .iter() .map(|_| Arc::new(AtomicUsize::new(0))) .collect(); - let gens: Vec> = names + let targets: Vec> = names .iter() - .zip(counters.iter()) - .map(|(name, calls)| configured(name, Arc::clone(calls))) + .zip(&counters) + .map(|(name, calls)| full(name, calls)) .collect(); - let mut orch = Orchestrator::new(); - for g in &gens { - orch = orch.with_generator(g); - } - - orch.run(&api, out_dir, &hooks, false).unwrap(); - - for (name, calls) in names.iter().zip(counters.iter()) { - assert_eq!( - calls.load(Ordering::SeqCst), - 1, - "generator '{name}' should have run exactly once", - ); - assert!( - out_dir.join(name).join("output.txt").exists(), - "generator '{name}' should have written its output", - ); + for t in &targets { + orch = orch.with_target(t); } - } - - #[test] - fn single_generator_cache_invalidates_independently() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let hooks = OrchestratorHooks::default(); - - let c_calls = Arc::new(AtomicUsize::new(0)); - let s_calls = Arc::new(AtomicUsize::new(0)); - let c_gen = configured("c", Arc::clone(&c_calls)); - let s_gen = configured("swift", Arc::clone(&s_calls)); - - let orch = Orchestrator::new() - .with_generator(&c_gen) - .with_generator(&s_gen); - - let api = test_api(); - orch.run(&api, out_dir, &hooks, false).unwrap(); - assert_eq!(c_calls.load(Ordering::SeqCst), 1); - assert_eq!(s_calls.load(Ordering::SeqCst), 1); - - // Mutate the API in a way that affects both generators' hashes by - // renaming a module. Then pre-seed the Swift cache with the *new* - // expected hash so only the C entry stays stale and re-runs. - let mut modified = api.api().clone(); - modified.modules[0].name = "math2".to_string(); - let modified = ResolvedApi::assume_resolved(modified); - - let new_swift_hash = - cache::hash_generator_inputs(modified.api(), "swift", &s_gen.config_hash_input()); - cache::write_generator_cache(out_dir, "swift", &new_swift_hash).unwrap(); - - orch.run(&modified, out_dir, &hooks, false).unwrap(); - assert_eq!( - c_calls.load(Ordering::SeqCst), - 2, - "C generator should re-run because its cache entry no longer matches", - ); - assert_eq!( - s_calls.load(Ordering::SeqCst), - 1, - "Swift generator's cache matched the new API and must be skipped", - ); - } - - #[test] - fn config_change_invalidates_cache() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let hooks = OrchestratorHooks::default(); - let api = test_api(); - - let calls = Arc::new(AtomicUsize::new(0)); - let g1 = ConfiguredGenerator::new( - CountingGenerator { - name: "counting", - calls: Arc::clone(&calls), - caps: TargetCapabilities::full(), - }, - TestConfig::default(), - ); - Orchestrator::new() - .with_generator(&g1) - .run(&api, out_dir, &hooks, false) - .unwrap(); - assert_eq!(calls.load(Ordering::SeqCst), 1); - - // Same generator, different config value: must re-run. - let g2 = ConfiguredGenerator::new( - CountingGenerator { - name: "counting", - calls: Arc::clone(&calls), - caps: TargetCapabilities::full(), - }, - TestConfig { - knob: Some("changed".into()), - allow_unsupported: false, - }, - ); - Orchestrator::new() - .with_generator(&g2) - .run(&api, out_dir, &hooks, false) + orch.run(&api(), out_dir(&dir), &OrchestratorHooks::default(), false) .unwrap(); - assert_eq!( - calls.load(Ordering::SeqCst), - 2, - "config-only change must invalidate the cache", - ); + for (name, calls) in names.iter().zip(&counters) { + assert_eq!(calls.load(Ordering::SeqCst), 1, "{name}"); + assert!(out_dir(&dir).join(name).join("output.txt").exists()); + } } } diff --git a/crates/weaveffi-core/src/errors.rs b/crates/weaveffi-core/src/errors.rs index cae26a96..c1f31e33 100644 --- a/crates/weaveffi-core/src/errors.rs +++ b/crates/weaveffi-core/src/errors.rs @@ -148,6 +148,7 @@ mod tests { fn module_with_errors(name: &str, codes: Vec<(&str, i32, &str)>) -> Module { Module { name: name.into(), + doc: None, functions: vec![], interfaces: vec![], structs: vec![], @@ -173,10 +174,8 @@ mod tests { fn api_with(mods: Vec) -> Api { Api { - version: "0.7.0".into(), - package: None, + version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), modules: mods, - generators: None, } } @@ -231,6 +230,7 @@ mod tests { fn no_domains_is_empty() { let api = api_with(vec![Module { name: "m".into(), + doc: None, functions: vec![], interfaces: vec![], structs: vec![], diff --git a/crates/weaveffi-core/src/lang.rs b/crates/weaveffi-core/src/lang.rs index ea5be460..f7616be8 100644 --- a/crates/weaveffi-core/src/lang.rs +++ b/crates/weaveffi-core/src/lang.rs @@ -223,12 +223,16 @@ pub const CSHARP_KEYWORDS: &[&str] = &[ ]; /// Dart reserved words, per the Dart language specification (the always -/// reserved set plus the builtin identifiers that cannot name locals). +/// reserved set plus the builtin identifiers that cannot name locals), plus +/// the lowercase `dart:core` type names (`int`, `double`, `num`, `bool`, +/// `dynamic`). Those are legal identifiers, but a field or parameter so named +/// shadows the type for the rest of its class or function body, so every +/// sibling declared as `int` stops resolving. pub const DART_KEYWORDS: &[&str] = &[ - "assert", "break", "case", "catch", "class", "const", "continue", "default", "do", "else", - "enum", "extends", "false", "final", "finally", "for", "if", "in", "is", "new", "null", - "rethrow", "return", "super", "switch", "this", "throw", "true", "try", "var", "void", "while", - "with", + "assert", "bool", "break", "case", "catch", "class", "const", "continue", "default", "do", + "double", "dynamic", "else", "enum", "extends", "false", "final", "finally", "for", "if", "in", + "int", "is", "new", "null", "num", "rethrow", "return", "super", "switch", "this", "throw", + "true", "try", "var", "void", "while", "with", ]; /// Ruby reserved words, per the Ruby language documentation. diff --git a/crates/weaveffi-core/src/lib.rs b/crates/weaveffi-core/src/lib.rs index 5e47ce12..5e5211df 100644 --- a/crates/weaveffi-core/src/lib.rs +++ b/crates/weaveffi-core/src/lib.rs @@ -1,4 +1,6 @@ -//! Core logic: Generator trait, codegen orchestration, validation, and shared utilities. +//! Core logic shared by every WeaveFFI generator and the CLI: validation, the +//! resolved API and binding model, the C ABI lowering, the marshalling plan, +//! the `LanguageBackend` trait, and code-generation orchestration. #![deny(missing_docs)] #![warn(clippy::missing_errors_doc)] #![warn(clippy::missing_panics_doc)] @@ -21,6 +23,5 @@ pub mod platform; pub mod resolved; pub mod utils; pub mod validate; -pub mod wire; pub use resolved::ResolvedApi; diff --git a/crates/weaveffi-core/src/model/mod.rs b/crates/weaveffi-core/src/model/mod.rs index 247fb1cf..63618fd2 100644 --- a/crates/weaveffi-core/src/model/mod.rs +++ b/crates/weaveffi-core/src/model/mod.rs @@ -1,35 +1,36 @@ //! The **binding model**: a normalized, fully-lowered view of a -//! [`ResolvedApi`] that every language backend -//! consumes. -//! -//! Before this module existed, each of the eleven generators re-walked the IR, -//! re-derived the C ABI calling convention, and re-invented every emitted C -//! symbol name. They drifted: iterators were lowered as lists in some targets, -//! listeners were emitted only by the C header, and a custom `c_prefix` reached -//! only the C and C++ outputs while the other nine hard-coded `weaveffi_`. +//! [`ResolvedApi`] that every language backend consumes. //! //! [`BindingModel::build`] walks the IR exactly once and produces a flat list //! of [`ModuleBinding`]s in which: //! -//! * every emitted **C symbol name** is precomputed once (so all backends agree -//! by construction, and a non-default prefix is honored everywhere); and -//! * every function/struct/callback is paired with its lowered [`AbiFn`] -//! signature (built from [`crate::abi`]), so no backend re-derives parameter -//! arity, ordering, or `out_*`/`out_err` placement. +//! * every **type** is a resolved [`Ty`] (its kind and owning module known, +//! no unresolved names), so a backend dispatches on [`Ty::family`] and +//! [`Ty::wire`] instead of re-deriving what a name means; +//! * every emitted **C symbol name** is precomputed once, so all backends +//! agree by construction and a non-default prefix is honored everywhere; and +//! * every function, callback, and interface member is paired with its +//! lowered [`AbiFn`] signature (built from [`crate::abi`]), so no backend +//! re-derives parameter arity, ordering, or `out_*`/`out_err` placement. //! -//! A backend reads the *idiomatic* shape from the retained [`TypeRef`]s -//! (`param.ty`, `field.ty`, …) and the *native* shape from the [`AbiFn`]s, then -//! writes only the marshalling that bridges the two in its own idioms. The hard, -//! drift-prone facts live here; only language syntax lives in the backends. +//! 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. The hard, drift-prone facts live here; +//! only language syntax lives in the backends. + +mod ty; + +pub use ty::{Family, Prim, Ty, WireType}; use heck::ToUpperCamelCase; use weaveffi_ir::ir::{ - CallbackDef, EnumDef, ErrorDomain, Function, InterfaceDef, ListenerDef, Module, StructDef, + CallbackDef, EnumDef, ErrorDomain, Function, InterfaceDef, Module, StructDef, StructField, TypeRef, }; use crate::abi::{ - self, async_callback_params, async_input_params, context_param, error_out_param, lower_param, + async_callback_params, async_input_params, context_param, error_out_param, lower_param, lower_return, sync_signature, AbiParam, CType, ConstPos, }; use crate::resolved::ResolvedApi; @@ -76,12 +77,12 @@ pub struct AsyncBinding { #[derive(Debug, Clone, PartialEq, Eq)] pub struct IteratorBinding { /// The element type `T` of `iter`. - pub elem: TypeRef, + pub elem: Ty, /// The opaque iterator tag (`{prefix}_{path}_{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, T* out_item, ..., error* out_err)`. pub next: AbiFn, /// `void {iter_tag}_destroy({iter_tag}* iter)`. pub destroy_symbol: String, @@ -92,8 +93,8 @@ pub struct IteratorBinding { pub struct ParamBinding { /// The parameter name as written in the IDL. pub name: String, - /// The idiomatic IR type a backend renders the parameter as. - pub ty: TypeRef, + /// The resolved type a backend renders the parameter as. + pub ty: Ty, /// Whether the parameter is mutable (drops the `const` on its pointer slots). pub mutable: bool, /// Optional doc comment carried from the IDL. @@ -102,6 +103,28 @@ pub struct ParamBinding { pub abi: Vec, } +impl ParamBinding { + /// Lower one parameter declared in the module whose underscore-joined C + /// path is `module`. + pub fn new( + name: impl Into, + ty: Ty, + mutable: bool, + doc: Option, + module: &str, + ) -> Self { + let name = name.into(); + let abi = lower_param(&name, &ty, module, mutable); + Self { + name, + ty, + mutable, + doc, + abi, + } + } +} + /// A function, fully lowered. /// /// Free functions and interface members share this shape. For an instance @@ -132,12 +155,13 @@ pub struct FnBinding { /// `true` for an instance method: the ABI signatures carry an implicit /// leading `self` slot not present in [`params`](Self::params). pub has_self: bool, - /// IR input parameters with their lowered slots. + /// Input parameters with their lowered slots. pub params: Vec, - /// The IR 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, + /// 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. @@ -157,8 +181,8 @@ pub struct FieldBinding { pub name: String, /// Optional doc comment carried from the IDL. pub doc: Option, - /// The idiomatic IR type of the field. - pub ty: TypeRef, + /// The resolved type of the field. + pub ty: Ty, } /// A struct (record), fully lowered: a plain value type generators emit as a @@ -170,6 +194,8 @@ pub struct StructBinding { pub name: String, /// Optional doc comment carried from the IDL. pub doc: Option, + /// Deprecation message when the struct is marked deprecated, else `None`. + pub deprecated: Option, /// The fields in declaration (and wire) order. pub fields: Vec, } @@ -189,6 +215,8 @@ pub struct EnumBinding { pub name: String, /// Optional doc comment carried from the IDL. pub doc: Option, + /// Deprecation message when the enum is marked deprecated, else `None`. + pub deprecated: Option, /// `{prefix}_{module_path}_{name}`. pub c_tag: String, /// Every variant, in declaration order. @@ -232,7 +260,7 @@ pub struct CallbackBinding { pub doc: Option, /// `{prefix}_{module_path}_{name}_fn`. pub c_fn_type: String, - /// IR parameters of the callback (without the trailing context). + /// Parameters of the callback (without the trailing context). pub params: Vec, /// The full ABI slot list, including the trailing `void* context`. pub abi_params: Vec, @@ -268,6 +296,8 @@ pub struct InterfaceBinding { pub name: String, /// Optional doc comment carried from the IDL. pub doc: Option, + /// Deprecation message when the interface is marked deprecated, else `None`. + pub deprecated: Option, /// `{prefix}_{module_path}_{name}`, the opaque tag. pub c_tag: String, /// Constructors, lowered as statics returning `{c_tag}*`. @@ -336,7 +366,11 @@ pub struct ModuleBinding { pub segments: Vec, /// Underscore-joined path used as the C symbol segment (e.g. `outer_inner`). pub path: String, - /// Module doc, taken from the first documented function in the module. + /// Dot-joined path used to qualify cross-module type names + /// (e.g. `outer.inner`). + pub dot_path: String, + /// The module's doc comment when the IDL records one, else the doc of the + /// first documented function in the module. pub doc: Option, /// The error domain in effect for this module's throwing functions: /// its own domain, the nearest ancestor's, or `None` when no domain is in @@ -391,6 +425,17 @@ impl ModuleBinding { .chain(i.statics.iter()) })) } + + /// `true` when any callable in this module is `async`. + pub fn has_async(&self) -> bool { + self.callables().any(|f| f.is_async) + } + + /// `true` when any callable in this module returns an iterator. + pub fn has_iterators(&self) -> bool { + self.callables() + .any(|f| matches!(f.shape, CallShape::Iterator(_))) + } } /// The whole API, normalized and lowered for code generation. @@ -411,8 +456,9 @@ impl BindingModel { /// the producer header and all consumers calling identical symbols. pub fn build(api: &ResolvedApi, prefix: &str) -> Self { let mut modules = Vec::new(); + let lowerer = Lowerer { api, prefix }; for m in &api.modules { - lower_module(m, &[], prefix, None, &mut modules); + lowerer.module(m, &[], None, &mut modules); } Self { prefix: prefix.to_string(), @@ -421,379 +467,478 @@ impl BindingModel { } } + /// The top-level modules (those with a single path segment), in order. + pub fn roots(&self) -> impl Iterator { + self.modules.iter().filter(|m| m.segments.len() == 1) + } + + /// The direct submodules of `parent`, in declaration order. Backends that + /// render nested namespaces recurse with this instead of re-walking the + /// IR tree. + 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[..] + }) + } + /// Iterate every function across all modules, paired with its module. pub fn functions(&self) -> impl Iterator { self.modules .iter() .flat_map(|m| m.functions.iter().map(move |f| (m, f))) } -} -/// Recursively lower `module` and its descendants into the flat `out` list, -/// pre-order (parent before children) so symbol declarations precede uses. -/// `inherited_error` is the nearest ancestor's error domain, threaded down so -/// every module knows which domain its throwing functions report. -fn lower_module( - module: &Module, - parent: &[String], - prefix: &str, - inherited_error: Option<&ErrorBinding>, - out: &mut Vec, -) { - let mut segments = parent.to_vec(); - segments.push(module.name.clone()); - let path = segments.join("_"); - - let error = match &module.errors { - Some(domain) => Some(lower_error_domain(domain, &path, prefix)), - None => inherited_error.cloned().map(|mut e| { - e.declared_here = false; - e - }), - }; + /// Iterate every callable (free functions and interface members) across + /// all modules, paired with its module. + pub fn callables(&self) -> impl Iterator { + self.modules + .iter() + .flat_map(|m| m.callables().map(move |f| (m, f))) + } - let enums = module - .enums - .iter() - .map(|e| lower_enum(e, &path, prefix)) - .collect(); - let structs = module.structs.iter().map(lower_struct).collect(); - let interfaces = module - .interfaces - .iter() - .map(|i| lower_interface(i, &path, prefix)) - .collect(); - let callbacks: Vec = module - .callbacks - .iter() - .map(|c| lower_callback(c, &path, prefix)) - .collect(); - let listeners = module - .listeners - .iter() - .map(|l| lower_listener(l, &path, prefix)) - .collect(); - let functions = module - .functions - .iter() - .map(|f| lower_function(f, &path, prefix)) - .collect(); - - // Module doc is synthesized from the first documented function, matching - // the `EmptyModuleDoc` lint's notion of "the module is documented". - let doc = module.functions.iter().find_map(|f| f.doc.clone()); - - out.push(ModuleBinding { - name: module.name.clone(), - segments: segments.clone(), - path, - doc, - error: error.clone(), - enums, - structs, - interfaces, - callbacks, - listeners, - functions, - }); - - for child in &module.modules { - lower_module(child, &segments, prefix, error.as_ref(), out); + /// `true` when any callable anywhere in the API is `async`. + pub fn has_async(&self) -> bool { + self.modules.iter().any(ModuleBinding::has_async) } -} -fn lower_error_domain(domain: &ErrorDomain, path: &str, prefix: &str) -> ErrorBinding { - let c_tag = format!("{prefix}_{path}_{}", domain.name); - ErrorBinding { - name: domain.name.clone(), - type_name: crate::errors::type_name(&domain.name, "Error"), - owner_path: path.to_string(), - declared_here: true, - c_tag: c_tag.clone(), - codes: domain - .codes - .iter() - .map(|c| ErrorCodeBinding { - name: c.name.clone(), - value: c.code, - message: c.message.clone(), - doc: c.doc.clone(), - c_const: format!("{c_tag}_{}", c.name), - fields: c.fields.iter().map(lower_field).collect(), - }) - .collect(), + /// `true` when any callable anywhere in the API returns an iterator. + pub fn has_iterators(&self) -> bool { + self.modules.iter().any(ModuleBinding::has_iterators) } -} -/// Lower one field of a record, variant payload, or error payload. -fn lower_field(f: &weaveffi_ir::ir::StructField) -> FieldBinding { - FieldBinding { - name: f.name.clone(), - doc: f.doc.clone(), - ty: f.ty.clone(), + /// `true` when the API declares any listener. + pub fn has_listeners(&self) -> bool { + self.modules.iter().any(|m| !m.listeners.is_empty()) } -} -/// 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 lower_interface(iface: &InterfaceDef, path: &str, prefix: &str) -> InterfaceBinding { - let c_tag = format!("{prefix}_{path}_{}", iface.name); - let self_slot = AbiParam::new( - "self", - CType::Ptr { - konst: ConstPos::West, - pointee: Box::new(CType::StructTag { - module: path.to_string(), - name: iface.name.clone(), - }), - }, - ); - let constructors = iface - .constructors - .iter() - .map(|c| { - // Synthesize the return: a constructor yields a new owned - // reference to the interface, exactly like a static returning it. - let mut f = c.clone(); - f.returns = Some(TypeRef::Interface(iface.name.clone())); - lower_callable(&f, path, prefix, &member_base(&c_tag, &c.name), None) - }) - .collect(); - let methods = iface - .methods - .iter() - .map(|m| { - lower_callable( - m, - path, - prefix, - &member_base(&c_tag, &m.name), - Some(self_slot.clone()), - ) + /// `true` when any type anywhere in the API crosses the ABI as a value + /// buffer (records, rich enums, optionals, lists, maps, error payloads). + pub fn has_buffers(&self) -> bool { + self.modules.iter().any(|m| { + !m.structs.is_empty() + || m.enums.iter().any(|e| e.rich) + || m.error + .as_ref() + .is_some_and(|e| e.codes.iter().any(|c| !c.fields.is_empty())) + }) || self.any_type(&|t| t.is_buffered()) + } + + /// Visit every boundary-crossing type in the API (callable and callback + /// parameters and returns, iterator elements, record, variant, and error + /// payload fields), recursing into composites, and return whether any + /// satisfies `pred`. Backends use this to decide which runtime helpers a + /// given API actually needs. + pub fn any_type(&self, pred: &dyn Fn(&Ty) -> bool) -> bool { + self.modules.iter().any(|m| { + m.callables().any(|f| { + f.params.iter().any(|p| p.ty.any(pred)) + || f.ret.as_ref().is_some_and(|r| r.any(pred)) + }) || m + .callbacks + .iter() + .any(|c| c.params.iter().any(|p| p.ty.any(pred))) + || m.structs + .iter() + .any(|s| s.fields.iter().any(|f| f.ty.any(pred))) + || m.enums.iter().any(|e| { + e.variants + .iter() + .any(|v| v.fields.iter().any(|f| f.ty.any(pred))) + }) + || m.error.as_ref().is_some_and(|e| { + e.declared_here + && e.codes + .iter() + .any(|c| c.fields.iter().any(|f| f.ty.any(pred))) + }) }) - .collect(); - let statics = iface - .statics - .iter() - .map(|s| lower_callable(s, path, prefix, &member_base(&c_tag, &s.name), None)) - .collect(); - InterfaceBinding { - name: iface.name.clone(), - doc: iface.doc.clone(), - c_tag: c_tag.clone(), - constructors, - methods, - statics, - destroy_symbol: format!("{c_tag}_destroy"), } } -/// The base C symbol of an interface member: `{c_tag}_{name}`. -fn member_base(c_tag: &str, name: &str) -> String { - format!("{c_tag}_{name}") +/// The per-build lowering context: the resolver and the symbol prefix. +struct Lowerer<'a> { + api: &'a ResolvedApi, + prefix: &'a str, } -fn lower_param_binding(p: &weaveffi_ir::ir::Param, module: &str) -> ParamBinding { - ParamBinding { - name: p.name.clone(), - ty: p.ty.clone(), - mutable: p.mutable, - doc: p.doc.clone(), - abi: lower_param(&p.name, &p.ty, module, p.mutable), - } +/// The per-module lowering context: where we are in the tree. +struct Scope<'a> { + /// Underscore-joined C path (`outer_inner`). + path: &'a str, + /// Dot-joined qualification path (`outer.inner`). + dot_path: &'a str, } -fn lower_enum(e: &EnumDef, path: &str, prefix: &str) -> EnumBinding { - let c_tag = format!("{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: v.fields.iter().map(lower_field).collect(), - }) - .collect(); - - EnumBinding { - name: e.name.clone(), - doc: e.doc.clone(), - c_tag, - variants, - rich: e.is_rich(), +impl Lowerer<'_> { + fn ty(&self, ty: &TypeRef, scope: &Scope<'_>) -> Ty { + self.api.resolve(ty, scope.dot_path) } -} -fn lower_struct(s: &StructDef) -> StructBinding { - StructBinding { - name: s.name.clone(), - doc: s.doc.clone(), - fields: s.fields.iter().map(lower_field).collect(), + /// Recursively lower `module` and its descendants into the flat `out` + /// list, pre-order (parent before children) so symbol declarations precede + /// uses. `inherited_error` is the nearest ancestor's error domain, + /// threaded down so every module knows which domain its throwing + /// functions report. + fn module( + &self, + module: &Module, + parent: &[String], + inherited_error: Option<&ErrorBinding>, + out: &mut Vec, + ) { + let mut segments = parent.to_vec(); + segments.push(module.name.clone()); + let path = segments.join("_"); + let dot_path = segments.join("."); + let scope = Scope { + path: &path, + dot_path: &dot_path, + }; + let prefix = self.prefix; + + let error = match &module.errors { + Some(domain) => Some(self.error_domain(domain, &scope)), + None => inherited_error.cloned().map(|mut e| { + e.declared_here = false; + e + }), + }; + + let enums = module + .enums + .iter() + .map(|e| self.enum_def(e, &scope)) + .collect(); + let structs = module + .structs + .iter() + .map(|s| self.struct_def(s, &scope)) + .collect(); + let interfaces = module + .interfaces + .iter() + .map(|i| self.interface(i, &scope)) + .collect(); + let callbacks = module + .callbacks + .iter() + .map(|c| self.callback(c, &scope)) + .collect(); + let listeners = module + .listeners + .iter() + .map(|l| ListenerBinding { + name: l.name.clone(), + doc: l.doc.clone(), + event_callback: l.event_callback.clone(), + callback_c_fn_type: format!("{prefix}_{path}_{}_fn", l.event_callback), + register_symbol: format!("{prefix}_{path}_register_{}", l.name), + unregister_symbol: format!("{prefix}_{path}_unregister_{}", l.name), + }) + .collect(); + let functions = module + .functions + .iter() + .map(|f| { + let c_base = format!("{prefix}_{path}_{}", f.name); + self.callable(f, &scope, &c_base, None) + }) + .collect(); + + let doc = module + .doc + .clone() + .or_else(|| module.functions.iter().find_map(|f| f.doc.clone())); + + out.push(ModuleBinding { + name: module.name.clone(), + segments: segments.clone(), + path: path.clone(), + dot_path: dot_path.clone(), + doc, + error: error.clone(), + enums, + structs, + interfaces, + callbacks, + listeners, + functions, + }); + + for child in &module.modules { + self.module(child, &segments, error.as_ref(), out); + } } -} -fn lower_callback(c: &CallbackDef, path: &str, prefix: &str) -> CallbackBinding { - let params: Vec = c - .params - .iter() - .map(|p| lower_param_binding(p, path)) - .collect(); - let mut abi_params: Vec = params.iter().flat_map(|p| p.abi.clone()).collect(); - abi_params.push(context_param()); - CallbackBinding { - name: c.name.clone(), - doc: c.doc.clone(), - c_fn_type: format!("{prefix}_{path}_{}_fn", c.name), - params, - abi_params, + fn error_domain(&self, domain: &ErrorDomain, scope: &Scope<'_>) -> ErrorBinding { + let c_tag = format!("{}_{}_{}", self.prefix, scope.path, domain.name); + ErrorBinding { + name: domain.name.clone(), + type_name: crate::errors::type_name(&domain.name, "Error"), + owner_path: scope.path.to_string(), + declared_here: true, + c_tag: c_tag.clone(), + codes: domain + .codes + .iter() + .map(|c| 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, scope), + }) + .collect(), + } } -} -fn lower_listener(l: &ListenerDef, path: &str, prefix: &str) -> ListenerBinding { - ListenerBinding { - name: l.name.clone(), - doc: l.doc.clone(), - event_callback: l.event_callback.clone(), - callback_c_fn_type: format!("{prefix}_{path}_{}_fn", l.event_callback), - register_symbol: format!("{prefix}_{path}_register_{}", l.name), - unregister_symbol: format!("{prefix}_{path}_unregister_{}", l.name), + fn fields(&self, fields: &[StructField], scope: &Scope<'_>) -> Vec { + fields + .iter() + .map(|f| FieldBinding { + name: f.name.clone(), + doc: f.doc.clone(), + ty: self.ty(&f.ty, scope), + }) + .collect() } -} -fn lower_function(f: &Function, path: &str, prefix: &str) -> FnBinding { - let c_base = format!("{prefix}_{path}_{}", f.name); - lower_callable(f, path, prefix, &c_base, None) -} + fn struct_def(&self, s: &StructDef, scope: &Scope<'_>) -> StructBinding { + StructBinding { + name: s.name.clone(), + doc: s.doc.clone(), + deprecated: s.deprecated.clone(), + fields: self.fields(&s.fields, scope), + } + } -/// 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 lower_callable( - f: &Function, - path: &str, - prefix: &str, - c_base: &str, - self_slot: Option, -) -> FnBinding { - let params: Vec = f - .params - .iter() - .map(|p| lower_param_binding(p, path)) - .collect(); - // 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()); + fn enum_def(&self, e: &EnumDef, scope: &Scope<'_>) -> EnumBinding { + let c_tag = format!("{}_{}_{}", self.prefix, scope.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, scope), + }) + .collect(); + EnumBinding { + name: e.name.clone(), + doc: e.doc.clone(), + deprecated: e.deprecated.clone(), + c_tag, + variants, + rich: e.is_rich(), } + } + + fn params(&self, params: &[weaveffi_ir::ir::Param], scope: &Scope<'_>) -> Vec { params - }; + .iter() + .map(|p| { + ParamBinding::new( + &p.name, + self.ty(&p.ty, scope), + p.mutable, + p.doc.clone(), + scope.path, + ) + }) + .collect() + } - let shape = if let Some(TypeRef::Iterator(inner)) = &f.returns { - 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}"); - - // launcher: (self,) input slots + out_err, returns iter_tag*. - let mut launch_params: Vec = f - .params + fn callback(&self, c: &CallbackDef, scope: &Scope<'_>) -> CallbackBinding { + let params = self.params(&c.params, scope); + let mut abi_params: Vec = params.iter().flat_map(|p| p.abi.clone()).collect(); + abi_params.push(context_param()); + CallbackBinding { + name: c.name.clone(), + doc: c.doc.clone(), + c_fn_type: format!("{}_{}_{}_fn", self.prefix, scope.path, c.name), + params, + abi_params, + } + } + + /// 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, scope: &Scope<'_>) -> InterfaceBinding { + let c_tag = format!("{}_{}_{}", self.prefix, scope.path, iface.name); + let self_slot = AbiParam::new( + "self", + CType::Ptr { + konst: ConstPos::West, + pointee: Box::new(CType::StructTag { + module: scope.path.to_string(), + name: iface.name.clone(), + }), + }, + ); + let member = |name: &str| format!("{c_tag}_{name}"); + let constructors = iface + .constructors .iter() - .flat_map(|p| lower_param(&p.name, &p.ty, path, p.mutable)) + .map(|c| { + // A constructor yields a new owned 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, scope, &member(&c.name), None) + }) .collect(); - 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 methods = iface + .methods + .iter() + .map(|m| self.callable(m, scope, &member(&m.name), Some(self_slot.clone()))) + .collect(); + let statics = iface + .statics + .iter() + .map(|s| self.callable(s, scope, &member(&s.name), None)) + .collect(); + InterfaceBinding { + name: iface.name.clone(), + doc: iface.doc.clone(), + deprecated: iface.deprecated.clone(), + c_tag: c_tag.clone(), + constructors, + methods, + statics, + destroy_symbol: format!("{c_tag}_destroy"), + } + } - // next: (iter, out_item, , out_err) -> int32. - let item = lower_return(inner, path); - 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, + /// 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, + scope: &Scope<'_>, + c_base: &str, + self_slot: Option, + ) -> FnBinding { + let prefix = self.prefix; + let path = scope.path; + let params = self.params(&f.params, scope); + let ret = f.returns.as_ref().map(|r| self.ty(r, scope)); + // 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 }; - CallShape::Iterator(IteratorBinding { - elem: (**inner).clone(), - iter_tag: iter_tag.clone(), - launch, - next, - destroy_symbol: format!("{iter_tag}_destroy"), - }) - } else if f.r#async { - let callback_type = format!("{c_base}_callback"); - let mut launch_params = async_input_params(f, path); - launch_params.push(AbiParam::new( - "callback", - CType::Named(format!("{core_base}_callback")), - )); - launch_params.push(context_param()); - let launch = AbiFn { - symbol: format!("{c_base}_async"), - params: with_self(launch_params), - ret: CType::Void, + 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: Vec = + params.iter().flat_map(|p| p.abi.iter().cloned()).collect(); + 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 item = lower_return(elem, path); + 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, + }; + + 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 callback_type = format!("{c_base}_callback"); + let mut launch_params = async_input_params(¶ms, f.cancellable); + launch_params.push(AbiParam::new( + "callback", + CType::Named(format!("{core_base}_callback")), + )); + launch_params.push(context_param()); + let launch = AbiFn { + symbol: format!("{c_base}_async"), + params: with_self(launch_params), + ret: CType::Void, + }; + CallShape::Async(AsyncBinding { + launch, + callback_type, + callback_params: async_callback_params(ret.as_ref(), path), + }) + } else { + let sig = sync_signature(¶ms, ret.as_ref(), path); + CallShape::Sync(AbiFn { + symbol: c_base.to_string(), + params: with_self(sig.params), + ret: sig.ret, + }) }; - CallShape::Async(AsyncBinding { - launch, - callback_type, - callback_params: async_callback_params(f.returns.as_ref(), path), - }) - } else { - let sig = sync_signature(&f.params, f.returns.as_ref(), path); - CallShape::Sync(AbiFn { - symbol: c_base.to_string(), - params: with_self(sig.params), - ret: sig.ret, - }) - }; - FnBinding { - name: f.name.clone(), - doc: f.doc.clone(), - deprecated: f.deprecated.clone(), - since: f.since.clone(), - cancellable: f.cancellable, - is_async: f.r#async, - throws: f.throws, - has_self: self_slot.is_some(), - params, - ret: f.returns.clone(), - c_base: c_base.to_string(), - shape, + FnBinding { + name: f.name.clone(), + doc: f.doc.clone(), + deprecated: f.deprecated.clone(), + since: f.since.clone(), + cancellable: f.cancellable, + is_async: f.r#async, + throws: f.throws, + has_self: self_slot.is_some(), + params, + ret, + c_base: c_base.to_string(), + shape, + } } } /// The element C type of an iterator's `out_item` slot (the pointee of /// `T* out_item`). Exposed for backends that materialize iterator results. -pub fn iterator_item_ctype(elem: &TypeRef, module: &str) -> CType { - abi::lower_return(elem, module).ret +pub fn iterator_item_ctype(elem: &Ty, module: &str) -> CType { + lower_return(elem, module).ret } #[cfg(test)] mod tests { use super::*; use weaveffi_ir::ir::{ - Api, CallbackDef, EnumDef, EnumVariant, Function, ListenerDef, Module, Param, StructDef, - StructField, + Api, CallbackDef, EnumDef, EnumVariant, Function, InterfaceDef, ListenerDef, Module, Param, + StructDef, StructField, }; fn param(name: &str, ty: TypeRef) -> Param { @@ -822,6 +967,7 @@ mod tests { fn module(name: &str) -> Module { Module { name: name.into(), + doc: None, functions: vec![], interfaces: vec![], structs: vec![], @@ -834,14 +980,19 @@ mod tests { } fn api(modules: Vec) -> ResolvedApi { - ResolvedApi::assume_resolved(Api { + ResolvedApi::assume_valid(Api { version: weaveffi_ir::ir::CURRENT_SCHEMA_VERSION.into(), modules, - generators: None, - package: None, }) } + 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 { @@ -855,33 +1006,18 @@ mod tests { let model = BindingModel::build(&api(vec![m]), "weaveffi"); let f = &model.modules[0].functions[0]; assert_eq!(f.c_base, "weaveffi_math_add"); - match &f.shape { - CallShape::Sync(abi) => { - assert_eq!(abi.symbol, "weaveffi_math_add"); - assert_eq!(abi.ret, CType::Int32); - let rendered: Vec = abi - .params - .iter() - .map(|p| format!("{} {}", p.ty.render_c("weaveffi"), p.name)) - .collect(); - assert_eq!( - rendered, - ["int32_t a", "int32_t b", "weaveffi_error* out_err"] - ); - } - _ => panic!("expected sync"), - } - } - - #[test] - fn prefix_is_honored_everywhere() { - let m = Module { - functions: vec![func("ping", vec![], None)], - ..module("net") + let CallShape::Sync(abi) = &f.shape else { + panic!("expected sync") }; - let model = BindingModel::build(&api(vec![m]), "acme"); - let f = &model.modules[0].functions[0]; - assert_eq!(f.c_base, "acme_net_ping"); + 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 = BindingModel::build(&api(vec![module("net")]), "acme"); + assert_eq!(model.prefix, "acme"); } #[test] @@ -889,7 +1025,6 @@ mod tests { let m = Module { functions: vec![Function { cancellable: true, - throws: false, r#async: true, ..func( "fetch", @@ -900,27 +1035,17 @@ mod tests { ..module("net") }; let model = BindingModel::build(&api(vec![m]), "weaveffi"); - match &model.modules[0].functions[0].shape { - CallShape::Async(a) => { - assert_eq!(a.launch.symbol, "weaveffi_net_fetch_async"); - assert_eq!(a.callback_type, "weaveffi_net_fetch_callback"); - let last_two: Vec<&str> = a - .launch - .params - .iter() - .rev() - .take(2) - .map(|p| p.name.as_str()) - .collect(); - assert_eq!(last_two, ["context", "callback"]); - // cancel_token slot is present before callback/context. - assert!(a.launch.params.iter().any(|p| p.name == "cancel_token")); - // callback prefix is (context, err, result). - assert_eq!(a.callback_params[0].name, "context"); - assert_eq!(a.callback_params[1].name, "err"); - } - _ => panic!("expected async"), - } + let CallShape::Async(a) = &model.modules[0].functions[0].shape else { + panic!("expected async") + }; + assert_eq!(a.launch.symbol, "weaveffi_net_fetch_async"); + 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"); + assert!(model.has_async()); } #[test] @@ -934,32 +1059,29 @@ mod tests { ..module("events") }; let model = BindingModel::build(&api(vec![m]), "weaveffi"); - match &model.modules[0].functions[0].shape { - CallShape::Iterator(it) => { - 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.next.ret, CType::Int32); - // out_item is `const char** out_item` for a string element. - let out_item = &it.next.params[1]; - assert_eq!(out_item.name, "out_item"); - assert_eq!(out_item.ty.render_c("weaveffi"), "const char**"); - } - _ => panic!("expected iterator"), - } + 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::StringUtf8); + assert_eq!(it.next.ret, CType::Int32); + assert_eq!(it.next.params[1].ty.render_c("weaveffi"), "const char**"); + assert!(model.has_iterators()); } #[test] - fn struct_is_a_value_type_schema() { - let m = Module { - interfaces: vec![], + 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(), @@ -967,88 +1089,97 @@ mod tests { doc: None, }, StructField { - name: "age".into(), - ty: TypeRef::I32, + name: "status".into(), + ty: TypeRef::Named("Status".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 model = BindingModel::build(&api(vec![m]), "weaveffi"); - let s = &model.modules[0].structs[0]; - // Records are value types: no C symbols, just the field schema in - // declaration (wire) order. - assert_eq!(s.name, "Contact"); - assert_eq!(s.fields.len(), 2); - assert_eq!(s.fields[0].name, "name"); - assert_eq!(s.fields[0].ty, TypeRef::StringUtf8); - assert_eq!(s.fields[1].name, "age"); - assert_eq!(s.fields[1].ty, TypeRef::I32); - } - - #[test] - fn record_params_and_returns_lower_to_buffers() { - let m = Module { + let other = Module { functions: vec![func( - "save", - vec![param("contact", TypeRef::Record("Contact".into()))], - Some(TypeRef::List(Box::new(TypeRef::Record("Contact".into())))), + "status_of", + vec![param("store", TypeRef::Named("Store".into()))], + Some(TypeRef::Named("Status".into())), )], - ..module("contacts") + ..module("ops") }; - let model = BindingModel::build(&api(vec![m]), "weaveffi"); - let f = &model.modules[0].functions[0]; - match &f.shape { - CallShape::Sync(abi) => { - let rendered: Vec = abi - .params - .iter() - .map(|p| format!("{} {}", p.ty.render_c("weaveffi"), p.name)) - .collect(); - assert_eq!( - rendered, - [ - "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*"); - } - _ => panic!("expected sync"), - } - } + let model = BindingModel::build(&api(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!(contacts.enums[0].c_tag, "weaveffi_contacts_Status"); + assert_eq!( + contacts.enums[0].variants[0].c_const, + "weaveffi_contacts_Status_Ok" + ); - #[test] - fn enum_constants_are_prefixed() { - let m = Module { - enums: vec![EnumDef { - name: "Color".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }], - ..module("gfx") + let iface = &contacts.interfaces[0]; + assert_eq!(iface.c_tag, "weaveffi_contacts_Store"); + 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") }; - let model = BindingModel::build(&api(vec![m]), "weaveffi"); - let e = &model.modules[0].enums[0]; - assert_eq!(e.c_tag, "weaveffi_gfx_Color"); - assert_eq!(e.variants[0].c_const, "weaveffi_gfx_Color_Red"); - assert_eq!(e.variants[1].c_const, "weaveffi_gfx_Color_Green"); + 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("contacts.Store".into())); + assert_eq!(f.ret, Some(Ty::Enum("contacts.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()); } #[test] @@ -1070,22 +1201,27 @@ mod tests { let mb = &model.modules[0]; let cb = &mb.callbacks[0]; assert_eq!(cb.c_fn_type, "weaveffi_events_on_message_fn"); - // context appended last. assert_eq!(cb.abi_params.last().unwrap().name, "context"); let l = &mb.listeners[0]; assert_eq!(l.register_symbol, "weaveffi_events_register_messages"); assert_eq!(l.unregister_symbol, "weaveffi_events_unregister_messages"); assert_eq!(l.callback_c_fn_type, "weaveffi_events_on_message_fn"); assert!(mb.callback("on_message").is_some()); + assert!(model.has_listeners()); + assert!(!model.has_buffers()); } #[test] - fn nested_modules_flatten_pre_order_with_paths() { + fn nested_modules_flatten_pre_order_with_paths_and_docs() { let inner = Module { - functions: vec![func("leaf_fn", vec![], None)], + 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") @@ -1093,9 +1229,12 @@ mod tests { let model = BindingModel::build(&api(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.as_deref(), Some("Leaf.")); } } diff --git a/crates/weaveffi-core/src/model/ty.rs b/crates/weaveffi-core/src/model/ty.rs new file mode 100644 index 00000000..85d72c94 --- /dev/null +++ b/crates/weaveffi-core/src/model/ty.rs @@ -0,0 +1,511 @@ +//! The **resolved type** every backend consumes, and the single taxonomy that +//! classifies it for the C ABI and the value-buffer wire format. +//! +//! The IDL's [`TypeRef`](weaveffi_ir::ir::TypeRef) is the type *as written*: +//! a user-defined type is a bare `Named` string because the parser cannot +//! know whether it names a record, an enum, or an interface. [`Ty`] is the +//! type *as resolved* by validation: every user reference carries its kind, +//! cross-module references are qualified with the owner's dot-joined module +//! path, and there is no "unresolved" variant for a backend to trip over. +//! +//! Three questions about a type used to be answered by separate match trees +//! in `abi`, `plan`, and `wire`, and each generator had its own copies on top. +//! They are answered once here: +//! +//! * [`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, or as an object pointer). The ABI lowering, the marshalling +//! plan, and every backend's argument and return handling dispatch on it. +//! * [`Ty::wire`]: how the type is encoded **inside a value buffer**. Every +//! backend's codec emitter dispatches on it. +//! * [`Ty::contains_user_type`]: whether encoding the type needs a +//! user-defined codec function somewhere. + +use std::fmt; + +/// A fully resolved type: the shape generators render and the ABI lowers. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Ty { + /// Signed 8-bit integer (`i8`). + I8, + /// Signed 16-bit integer (`i16`). + I16, + /// Signed 32-bit integer (`i32`). + I32, + /// Signed 64-bit integer (`i64`). + I64, + /// Unsigned 8-bit integer (`u8`). + U8, + /// Unsigned 16-bit integer (`u16`). + U16, + /// Unsigned 32-bit integer (`u32`). + U32, + /// Unsigned 64-bit integer (`u64`). + U64, + /// 32-bit IEEE 754 floating-point number (`f32`). + F32, + /// 64-bit IEEE 754 floating-point number (`f64`). + F64, + /// Boolean (`bool`). + Bool, + /// Owned UTF-8 string (`string`). + StringUtf8, + /// Owned byte buffer (`bytes`). + Bytes, + /// Borrowed string slice (`&str`): valid only for the duration of a call. + BorrowedStr, + /// Borrowed byte slice (`&[u8]`): valid only for the duration of a call. + BorrowedBytes, + /// Opaque, untyped resource handle (`handle`). + Handle, + /// Opaque resource handle tagged with the (possibly dot-qualified) name + /// of what it refers to (`handle`). + TypedHandle(String), + /// 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. + /// The name is dot-qualified when the record lives in another module. + 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. + Enum(String), + /// A user interface: an opaque object reference. As a parameter the + /// object is borrowed for the call; as a return the caller receives a new + /// owned reference it must eventually release. + Interface(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), +} + +/// 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. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Family { + /// One C slot passed by value: scalars, bools, C-style enums, and handles + /// (a typed handle's slot is an opaque pointer, still by value). + Direct, + /// One `char*` slot. Borrowed for a call as a parameter; producer-owned + /// and released with `{prefix}_free_string` as a return. + String, + /// A `(ptr, len)` raw byte pair. Borrowed as a parameter; producer-owned + /// and released with `{prefix}_free_bytes` as a return. + 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. + Buffer, + /// An opaque object pointer to an interface. Borrowed as a parameter; + /// an owned reference (released with its `_destroy` symbol) as a return. + Object { + /// `true` for `Interface?`: null is a legal "none" value. + nullable: bool, + }, + /// An `iter` return: an opaque iterator handle with its own + /// `next`/`destroy` protocol. Never a parameter. + Iterator, +} + +/// A fixed-width wire primitive inside a value buffer. +/// +/// Every backend spells the read/write routine for one of these the same way +/// modulo casing (`read_i32`, `readI32`, `ReadI32`), so the shared name +/// helpers let a codec emitter dispatch on the whole set with one arm. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Prim { + /// One byte, `0` or `1`. + Bool, + /// One signed byte. + I8, + /// Two bytes, little-endian, signed. + I16, + /// Four bytes, little-endian, signed. + I32, + /// Eight bytes, little-endian, signed. + I64, + /// One unsigned byte. + U8, + /// Two bytes, little-endian, unsigned. + U16, + /// Four bytes, little-endian, unsigned. + U32, + /// Eight bytes, little-endian, unsigned. + U64, + /// Four bytes, IEEE 754 bits. + F32, + /// Eight bytes, IEEE 754 bits. + F64, + /// A `u32` byte length followed by UTF-8 bytes, no NUL terminator. + /// Covers `string` and `&str`. + String, + /// A `u32` length followed by raw bytes. Covers `bytes` and `&[u8]`. + Bytes, +} + +impl Prim { + /// Every primitive, for exhaustive tables in runtime preambles. + pub const ALL: [Prim; 13] = [ + Prim::Bool, + Prim::I8, + Prim::I16, + Prim::I32, + Prim::I64, + Prim::U8, + Prim::U16, + Prim::U32, + Prim::U64, + Prim::F32, + Prim::F64, + Prim::String, + Prim::Bytes, + ]; + + /// The lower-case spelling (`bool`, `i32`, `string`, `bytes`), the stem + /// of snake-case routine names such as `read_i32`. + pub fn snake(self) -> &'static str { + match self { + Prim::Bool => "bool", + Prim::I8 => "i8", + Prim::I16 => "i16", + Prim::I32 => "i32", + Prim::I64 => "i64", + Prim::U8 => "u8", + Prim::U16 => "u16", + Prim::U32 => "u32", + Prim::U64 => "u64", + Prim::F32 => "f32", + Prim::F64 => "f64", + Prim::String => "string", + Prim::Bytes => "bytes", + } + } + + /// The capitalized spelling (`Bool`, `I32`, `String`, `Bytes`), the stem + /// of camel-case routine names such as `readI32`. + pub fn pascal(self) -> &'static str { + match self { + Prim::Bool => "Bool", + Prim::I8 => "I8", + Prim::I16 => "I16", + Prim::I32 => "I32", + Prim::I64 => "I64", + Prim::U8 => "U8", + Prim::U16 => "U16", + Prim::U32 => "U32", + Prim::U64 => "U64", + Prim::F32 => "F32", + Prim::F64 => "F64", + Prim::String => "String", + Prim::Bytes => "Bytes", + } + } + + /// `true` for the integer and float primitives (everything but `bool`, + /// `string`, and `bytes`). + pub fn is_numeric(self) -> bool { + !matches!(self, Prim::Bool | Prim::String | Prim::Bytes) + } +} + +/// The closed set of shapes a value inside a value buffer can take. +/// +/// This is the dispatch alphabet for every backend's buffer codec: one +/// variant per encode/decode primitive of the wire format. The borrowed +/// references point back into the classified [`Ty`], so classification +/// allocates nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WireType<'a> { + /// A fixed-width primitive, a string, or a byte blob. + Prim(Prim), + /// An opaque handle token: eight bytes, little-endian, unsigned. Carries + /// the typed handle's referent name (if any) so backends that wrap + /// handles in typed structs can pick the wrapper; the encoding is the + /// same either way. + Handle(Option<&'a str>), + /// A C-style enum: an `i32` discriminant. Carries the (possibly + /// dot-qualified) enum name so backends can wrap the integer in their + /// typed enum. + Enum(&'a str), + /// A record or rich enum: the named type's own codec applies (fields in + /// declaration order for a record; an `i32` tag plus the active + /// variant's fields for a rich enum). Backends emit one codec function + /// per user type and delegate here by name (possibly dot-qualified). + User(&'a str), + /// A one-byte presence flag (`0` absent, `1` present) followed by the + /// inner value when present. + Optional(&'a Ty), + /// A `u32` element count followed by each element. + List(&'a Ty), + /// A `u32` entry count followed by alternating key and value. + Map(&'a Ty, &'a Ty), +} + +impl Ty { + /// How this type crosses a call boundary. Total over every `Ty`. + pub fn family(&self) -> Family { + match self { + Ty::StringUtf8 | Ty::BorrowedStr => Family::String, + Ty::Bytes | Ty::BorrowedBytes => Family::Bytes, + Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => Family::Buffer, + Ty::Interface(_) => Family::Object { nullable: false }, + // The one optional that is not buffered: an object reference + // cannot be serialized by value, so `Interface?` stays a nullable + // pointer. + Ty::Optional(inner) if matches!(inner.as_ref(), Ty::Interface(_)) => { + Family::Object { nullable: true } + } + Ty::Optional(_) => Family::Buffer, + Ty::Iterator(_) => Family::Iterator, + Ty::I8 + | Ty::I16 + | Ty::I32 + | Ty::I64 + | Ty::U8 + | Ty::U16 + | Ty::U32 + | Ty::U64 + | Ty::F32 + | Ty::F64 + | Ty::Bool + | Ty::Handle + | Ty::TypedHandle(_) + | Ty::Enum(_) => Family::Direct, + } + } + + /// `true` when this type crosses the C ABI as a serialized value buffer + /// (`const uint8_t*` + `size_t`) rather than as dedicated C slots. + pub fn is_buffered(&self) -> bool { + self.family() == Family::Buffer + } + + /// `true` for the two borrowed views (`&str`, `&[u8]`). + pub fn is_borrowed(&self) -> bool { + matches!(self, Ty::BorrowedStr | Ty::BorrowedBytes) + } + + /// The referenced user-type name for a record, rich enum, enum, + /// interface, or typed handle, or `None` for every other type. + pub fn user_name(&self) -> Option<&str> { + match self { + Ty::Record(n) + | Ty::RichEnum(n) + | Ty::Enum(n) + | Ty::Interface(n) + | Ty::TypedHandle(n) => Some(n), + _ => None, + } + } + + /// The interface name inside a bare or optional interface type, or + /// `None` when the type is not an object reference. + pub fn interface_name(&self) -> Option<&str> { + match self { + Ty::Interface(n) => Some(n), + Ty::Optional(inner) => inner.interface_name(), + _ => None, + } + } + + /// Classify this type's encoding inside a value buffer. + /// + /// Total over every type validation admits inside a buffered position. + /// 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 an interface or an iterator, neither of which + /// can legally appear inside a value buffer. + pub fn wire(&self) -> WireType<'_> { + match self { + Ty::Bool => WireType::Prim(Prim::Bool), + Ty::I8 => WireType::Prim(Prim::I8), + Ty::I16 => WireType::Prim(Prim::I16), + Ty::I32 => WireType::Prim(Prim::I32), + Ty::I64 => WireType::Prim(Prim::I64), + Ty::U8 => WireType::Prim(Prim::U8), + Ty::U16 => WireType::Prim(Prim::U16), + Ty::U32 => WireType::Prim(Prim::U32), + Ty::U64 => WireType::Prim(Prim::U64), + Ty::F32 => WireType::Prim(Prim::F32), + Ty::F64 => WireType::Prim(Prim::F64), + Ty::StringUtf8 | Ty::BorrowedStr => WireType::Prim(Prim::String), + Ty::Bytes | Ty::BorrowedBytes => WireType::Prim(Prim::Bytes), + Ty::Handle => WireType::Handle(None), + Ty::TypedHandle(n) => WireType::Handle(Some(n)), + Ty::Enum(name) => WireType::Enum(name), + Ty::Record(name) | Ty::RichEnum(name) => WireType::User(name), + Ty::Optional(inner) => WireType::Optional(inner), + Ty::List(inner) => WireType::List(inner), + Ty::Map(k, v) => WireType::Map(k, v), + Ty::Interface(_) | Ty::Iterator(_) => { + panic!("object references cannot appear inside value buffers: {self}") + } + } + } + + /// `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. + pub fn contains_user_type(&self) -> bool { + match self { + Ty::Record(_) | Ty::RichEnum(_) => true, + Ty::Optional(inner) | Ty::List(inner) | Ty::Iterator(inner) => { + inner.contains_user_type() + } + Ty::Map(k, v) => k.contains_user_type() || v.contains_user_type(), + _ => false, + } + } + + /// `true` when `pred` holds for this type or any type nested inside it + /// (optional payloads, list and iterator 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::Map(k, v) => k.any(pred) || v.any(pred), + _ => false, + } + } + + /// The element type of an `iter`, or `None` for any other type. + pub fn iterator_elem(&self) -> Option<&Ty> { + match self { + Ty::Iterator(inner) => Some(inner), + _ => None, + } + } +} + +impl fmt::Display for Ty { + /// Renders the IDL spelling (`i32`, `[string]`, `{string:i32}`, + /// `Contact?`, `iter`), which is what diagnostics and generated + /// doc comments quote. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Ty::I8 => f.write_str("i8"), + Ty::I16 => f.write_str("i16"), + Ty::I32 => f.write_str("i32"), + Ty::I64 => f.write_str("i64"), + Ty::U8 => f.write_str("u8"), + Ty::U16 => f.write_str("u16"), + Ty::U32 => f.write_str("u32"), + Ty::U64 => f.write_str("u64"), + Ty::F32 => f.write_str("f32"), + Ty::F64 => f.write_str("f64"), + Ty::Bool => f.write_str("bool"), + Ty::StringUtf8 => f.write_str("string"), + Ty::Bytes => f.write_str("bytes"), + Ty::BorrowedStr => f.write_str("&str"), + Ty::BorrowedBytes => f.write_str("&[u8]"), + Ty::Handle => f.write_str("handle"), + Ty::TypedHandle(n) => write!(f, "handle<{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}>"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn families_are_total_and_agree_with_the_abi_contract() { + assert_eq!(Ty::I32.family(), Family::Direct); + assert_eq!(Ty::Handle.family(), Family::Direct); + assert_eq!(Ty::TypedHandle("S".into()).family(), Family::Direct); + assert_eq!(Ty::Enum("Color".into()).family(), Family::Direct); + assert_eq!(Ty::StringUtf8.family(), Family::String); + assert_eq!(Ty::BorrowedStr.family(), Family::String); + assert_eq!(Ty::Bytes.family(), Family::Bytes); + assert_eq!(Ty::BorrowedBytes.family(), Family::Bytes); + for ty in [ + Ty::Record("C".into()), + Ty::RichEnum("S".into()), + Ty::List(Box::new(Ty::I32)), + Ty::Map(Box::new(Ty::StringUtf8), Box::new(Ty::I32)), + Ty::Optional(Box::new(Ty::I32)), + Ty::Optional(Box::new(Ty::StringUtf8)), + Ty::Optional(Box::new(Ty::Record("C".into()))), + ] { + assert_eq!(ty.family(), Family::Buffer, "{ty}"); + assert!(ty.is_buffered()); + } + assert_eq!( + Ty::Interface("Store".into()).family(), + Family::Object { nullable: false } + ); + assert_eq!( + Ty::Optional(Box::new(Ty::Interface("Store".into()))).family(), + Family::Object { nullable: true } + ); + assert_eq!(Ty::Iterator(Box::new(Ty::I32)).family(), Family::Iterator); + } + + #[test] + fn wire_folds_borrowed_and_handles() { + assert_eq!(Ty::BorrowedStr.wire(), WireType::Prim(Prim::String)); + assert_eq!(Ty::BorrowedBytes.wire(), WireType::Prim(Prim::Bytes)); + assert_eq!(Ty::Handle.wire(), WireType::Handle(None)); + assert_eq!( + Ty::TypedHandle("Session".into()).wire(), + WireType::Handle(Some("Session")) + ); + 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(Ty::I32)); + assert_eq!(list.wire(), WireType::List(&Ty::I32)); + } + + #[test] + #[should_panic(expected = "object references")] + fn interfaces_have_no_wire_shape() { + Ty::Interface("Store".into()).wire(); + } + + #[test] + fn user_type_containment_recurses() { + assert!(Ty::Record("C".into()).contains_user_type()); + assert!(Ty::Map( + Box::new(Ty::StringUtf8), + 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()); + } + + #[test] + fn display_is_the_idl_spelling() { + let ty = Ty::List(Box::new(Ty::Map( + Box::new(Ty::StringUtf8), + Box::new(Ty::Optional(Box::new(Ty::Record("a.Contact".into())))), + ))); + assert_eq!(ty.to_string(), "[{string:a.Contact?}]"); + assert_eq!( + Ty::Iterator(Box::new(Ty::BorrowedStr)).to_string(), + "iter<&str>" + ); + assert_eq!(Prim::String.snake(), "string"); + assert_eq!(Prim::I32.pascal(), "I32"); + } +} diff --git a/crates/weaveffi-core/src/pkg.rs b/crates/weaveffi-core/src/pkg.rs index 8df17163..5b68e4a9 100644 --- a/crates/weaveffi-core/src/pkg.rs +++ b/crates/weaveffi-core/src/pkg.rs @@ -1,25 +1,54 @@ //! Shared package-identity resolution. //! -//! A single `package:` block in the IDL ([`weaveffi_ir::ir::Package`]) is the +//! The `[package]` table of a project's `weaveffi.toml` ([`Package`]) is the //! source of truth for the name, version, and metadata stamped into every //! ecosystem manifest (`package.json`, `pyproject.toml`, `*.gemspec`, //! `*.csproj`, `pubspec.yaml`, `Package.swift`, `build.gradle`, `go.mod`). +//! The CLI attaches it to the [`ResolvedApi`] it hands every backend. //! //! This module centralizes the *resolution* rules (precedence and defaults) //! so all eleven generators agree on the package identity instead of each one -//! hardcoding `weaveffi` / `0.1.0`. Generators read [`resolve`] in their +//! hardcoding `weaveffi` / `0.1.0`. Generators call [`resolve`] in their //! manifest code and map the [`ResolvedPackage`] fields onto whatever their //! ecosystem's manifest format requires. -use weaveffi_ir::ir::Api; +use serde::{Deserialize, Serialize}; -/// Fallback package version when the IDL omits `package.version`. +use crate::resolved::ResolvedApi; + +/// Fallback package version when the project omits `package.version`. pub const DEFAULT_VERSION: &str = "0.1.0"; -/// Fallback package name when the IDL omits `package.name` and no per-target -/// override or input basename is available. +/// Fallback package name when the project omits `package.name` and no +/// per-target override or input basename is available. pub const DEFAULT_NAME: &str = "weaveffi"; +/// Package identity and metadata declared in the `[package]` table of +/// `weaveffi.toml`. +/// +/// Every field is optional in the file; [`resolve`] applies the defaults and +/// precedence rules. The same struct is what `weaveffi generate` hashes into +/// each target's cache key, so editing the table regenerates every target. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct Package { + /// Distribution name used across every ecosystem unless a per-target + /// override (e.g. `python.package_name`) supplies one. + pub name: Option, + /// Semantic version stamped into every manifest. + pub version: Option, + /// Short package description. + pub description: Option, + /// License identifier, typically an SPDX expression. + pub license: Option, + /// Package authors. + pub authors: Vec, + /// Project homepage URL. + pub homepage: Option, + /// Source repository URL. + pub repository: Option, +} + /// Package identity resolved for one generator/manifest. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolvedPackage { @@ -27,12 +56,12 @@ pub struct ResolvedPackage { pub name: String, /// Semantic version stamped into the manifest. pub version: String, - /// Short package description, or `None` when the IDL omits it. See + /// Short package description, or `None` when unset. See /// [`description_or_default`](Self::description_or_default) for the fallback. pub description: Option, /// License identifier (typically an SPDX expression), or `None` when unset. pub license: Option, - /// Package authors, taken verbatim from the `package:` block (empty when + /// Package authors, taken verbatim from the `[package]` table (empty when /// none are declared). pub authors: Vec, /// Project homepage URL, or `None` when unset. @@ -42,7 +71,7 @@ pub struct ResolvedPackage { } impl ResolvedPackage { - /// Human-readable description, or a generated default when the IDL omits it. + /// Human-readable description, or a generated default when unset. pub fn description_or_default(&self) -> String { self.description .clone() @@ -52,14 +81,14 @@ impl ResolvedPackage { /// The `name` rewritten so it is a legal lower-snake identifier (e.g. a /// Python import package or a Ruby `require` path): non-alphanumerics - /// collapse to `_`. `"my-kv.store"` → `"my_kv_store"`. + /// collapse to `_`. `"my-kv.store"` becomes `"my_kv_store"`. pub fn ident_name(&self) -> String { sanitize_ident(&self.name) } /// The `name` rewritten as an UpperCamelCase identifier suitable for a /// language-level module or namespace (Ruby `module`, .NET `namespace`, - /// Swift module, Dart class prefix). `"my-kv.store"` → `"MyKvStore"`. + /// Swift module, Dart class prefix). `"my-kv.store"` becomes `"MyKvStore"`. pub fn module_name(&self) -> String { pascal_ident(&self.name) } @@ -67,7 +96,8 @@ impl ResolvedPackage { /// UpperCamelCase identifier-safe form of an arbitrary package name. Word /// boundaries are any run of non-alphanumerics (and existing camel humps are -/// preserved). `"my-kv.store"` → `"MyKvStore"`, `"kvstore"` → `"Kvstore"`. +/// preserved). `"my-kv.store"` becomes `"MyKvStore"`, `"kvstore"` becomes +/// `"Kvstore"`. pub fn pascal_ident(name: &str) -> String { let mut out = String::with_capacity(name.len()); let mut start_word = true; @@ -112,8 +142,8 @@ pub fn sanitize_ident(name: &str) -> String { } /// Strip directory and extension from an IDL basename to use as a fallback -/// package name. `"path/kvstore.yml"` → `"kvstore"`, `None`/empty → -/// [`DEFAULT_NAME`]. +/// package name. `"path/kvstore.yml"` becomes `"kvstore"`; `None` or empty +/// becomes [`DEFAULT_NAME`]. pub fn name_from_basename(basename: Option<&str>) -> String { basename .and_then(|b| b.rsplit(['/', '\\']).next()) @@ -123,72 +153,65 @@ pub fn name_from_basename(basename: Option<&str>) -> String { .to_string() } +fn non_empty(s: Option<&String>) -> Option { + s.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()) +} + /// Resolve package identity for a generator. /// /// Name precedence (first non-empty wins): /// 1. explicit per-target `name_override` (e.g. `python.package_name`), -/// 2. `api.package.name`, +/// 2. `[package] name` from the project's `weaveffi.toml`, /// 3. the IDL file stem (`input_basename`), /// 4. [`DEFAULT_NAME`]. /// -/// Version: `api.package.version` → [`DEFAULT_VERSION`]. All other metadata is -/// taken verbatim from the `package:` block (absent → `None`/empty). +/// Version: `[package] version`, then [`DEFAULT_VERSION`]. All other metadata +/// is taken verbatim from the `[package]` table (absent becomes `None` or +/// empty). pub fn resolve( - api: &Api, + api: &ResolvedApi, name_override: Option<&str>, input_basename: Option<&str>, ) -> ResolvedPackage { - let pkg = api.package.as_ref(); + let pkg = api.package(); let name = name_override .map(str::trim) .filter(|s| !s.is_empty()) .map(str::to_string) - .or_else(|| { - pkg.map(|p| p.name.trim().to_string()) - .filter(|s| !s.is_empty()) - }) + .or_else(|| non_empty(pkg.and_then(|p| p.name.as_ref()))) .unwrap_or_else(|| name_from_basename(input_basename)); - let version = pkg - .map(|p| p.version.trim().to_string()) - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| DEFAULT_VERSION.to_string()); ResolvedPackage { name, - version, - description: pkg - .and_then(|p| p.description.clone()) - .filter(|s| !s.is_empty()), - license: pkg - .and_then(|p| p.license.clone()) - .filter(|s| !s.is_empty()), + version: non_empty(pkg.and_then(|p| p.version.as_ref())) + .unwrap_or_else(|| DEFAULT_VERSION.to_string()), + description: non_empty(pkg.and_then(|p| p.description.as_ref())), + license: non_empty(pkg.and_then(|p| p.license.as_ref())), authors: pkg.map(|p| p.authors.clone()).unwrap_or_default(), - homepage: pkg - .and_then(|p| p.homepage.clone()) - .filter(|s| !s.is_empty()), - repository: pkg - .and_then(|p| p.repository.clone()) - .filter(|s| !s.is_empty()), + homepage: non_empty(pkg.and_then(|p| p.homepage.as_ref())), + repository: non_empty(pkg.and_then(|p| p.repository.as_ref())), } } #[cfg(test)] mod tests { use super::*; - use weaveffi_ir::ir::Package; + use weaveffi_ir::ir::{Api, CURRENT_SCHEMA_VERSION}; - fn api_with(pkg: Option) -> Api { - Api { - version: "0.7.0".into(), - package: pkg, + fn api_with(pkg: Option) -> ResolvedApi { + let api = ResolvedApi::assume_valid(Api { + version: CURRENT_SCHEMA_VERSION.into(), modules: vec![], - generators: None, + }); + match pkg { + Some(p) => api.with_package(p), + None => api, } } fn full_pkg() -> Package { Package { - name: "kvstore".into(), - version: "1.2.0".into(), + name: Some("kvstore".into()), + version: Some("1.2.0".into()), description: Some("KV store".into()), license: Some("MIT".into()), authors: vec!["Ada".into()], @@ -198,21 +221,15 @@ mod tests { } #[test] - fn package_block_drives_identity() { + fn package_table_drives_identity_and_overrides_win() { let api = api_with(Some(full_pkg())); let r = resolve(&api, None, Some("ignored.yml")); assert_eq!(r.name, "kvstore"); assert_eq!(r.version, "1.2.0"); assert_eq!(r.license.as_deref(), Some("MIT")); assert_eq!(r.authors, vec!["Ada".to_string()]); - } - - #[test] - fn target_override_beats_package_name() { - let api = api_with(Some(full_pkg())); let r = resolve(&api, Some("kvstore_py"), Some("kvstore.yml")); assert_eq!(r.name, "kvstore_py"); - // Version still comes from the package block. assert_eq!(r.version, "1.2.0"); } @@ -222,33 +239,30 @@ mod tests { let r = resolve(&api, None, Some("path/to/contacts.yml")); assert_eq!(r.name, "contacts"); assert_eq!(r.version, DEFAULT_VERSION); - - let r2 = resolve(&api, None, None); - assert_eq!(r2.name, DEFAULT_NAME); - } - - #[test] - fn description_default_is_generated() { - let api = api_with(None); - let r = resolve(&api, Some("widgets"), None); + assert_eq!(resolve(&api, None, None).name, DEFAULT_NAME); assert_eq!( - r.description_or_default(), + resolve(&api, Some("widgets"), None).description_or_default(), "widgets bindings generated by WeaveFFI" ); } #[test] - fn ident_name_sanitizes() { - assert_eq!(sanitize_ident("my-kv.store"), "my_kv_store"); - assert_eq!(sanitize_ident("Kvstore"), "kvstore"); - assert_eq!(sanitize_ident("--"), DEFAULT_NAME); + fn package_table_parses_from_toml() { + let pkg: Package = + toml::from_str("name = \"kv\"\nversion = \"2.0.0\"\nauthors = [\"A\", \"B\"]\n") + .unwrap(); + assert_eq!(pkg.name.as_deref(), Some("kv")); + assert_eq!(pkg.authors.len(), 2); + assert!(toml::from_str::("bogus = 1").is_err()); } #[test] - fn pascal_ident_upper_camels() { + fn identifiers_sanitize() { + assert_eq!(sanitize_ident("my-kv.store"), "my_kv_store"); + assert_eq!(sanitize_ident("Kvstore"), "kvstore"); + assert_eq!(sanitize_ident("--"), DEFAULT_NAME); assert_eq!(pascal_ident("my-kv.store"), "MyKvStore"); assert_eq!(pascal_ident("kvstore"), "Kvstore"); - assert_eq!(pascal_ident("contacts"), "Contacts"); assert_eq!(pascal_ident("--"), "Weaveffi"); } } diff --git a/crates/weaveffi-core/src/plan.rs b/crates/weaveffi-core/src/plan.rs index bf8c39be..8af26c3b 100644 --- a/crates/weaveffi-core/src/plan.rs +++ b/crates/weaveffi-core/src/plan.rs @@ -12,9 +12,9 @@ //! * **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? -//! * **Ownership** ([`ReturnFree`], [`ElemFree`]): after copying a returned -//! value into a native one, exactly which runtime release call does the -//! wrapper owe, if any? +//! * **Ownership** ([`RetPass::free`], [`ElemFree`]): after copying a +//! returned value into a native one, exactly which runtime release call +//! does the wrapper owe, if any? //! * **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). @@ -22,14 +22,13 @@ //! including the rule that results and errors are owned by the consumer //! and released through the runtime free symbols. //! -//! A backend that renders these plans in its own syntax cannot drift from the -//! others on semantics; only the spelling differs. +//! Every classification here derives from [`Ty::family`], so a backend that +//! renders these plans in its own syntax cannot drift from the others on +//! semantics; only the spelling differs. -use weaveffi_ir::ir::TypeRef; - -use crate::abi::lower::{is_buffered, split_qualified}; +use crate::abi::split_qualified; use crate::abi::AbiParam; -use crate::model::{AsyncBinding, FnBinding, IteratorBinding, ParamBinding}; +use crate::model::{AsyncBinding, Family, FnBinding, IteratorBinding, ParamBinding, Ty}; /// How a callable's `out_err` slot is interpreted by idiomatic wrappers. /// @@ -99,8 +98,8 @@ pub enum ArgPass<'a> { }, /// A buffered value (record, rich enum, optional, list, or map): the /// wrapper serializes it into the value-buffer wire format - /// ([`crate::wire`]), passes the encoding as a borrowed `(ptr, len)` - /// pair, and releases its own encoding after the call returns. + /// ([`Ty::wire`]), passes the encoding as a borrowed `(ptr, len)` pair, + /// and releases its own encoding after the call returns. Buffer { /// The `const uint8_t*` data slot. ptr: &'a AbiParam, @@ -123,8 +122,8 @@ impl ParamBinding { /// /// # Panics /// - /// Panics if the parameter's precomputed ABI slots disagree with its IR - /// type's shape, which would be a bug in the model construction, not a + /// 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 = || { @@ -143,39 +142,44 @@ impl ParamBinding { ); &self.abi[0] }; - if is_buffered(&self.ty) { - let (ptr, len) = pair(); - return ArgPass::Buffer { ptr, len }; - } - match &self.ty { - TypeRef::StringUtf8 | TypeRef::BorrowedStr => ArgPass::String { slot: single() }, - TypeRef::Bytes | TypeRef::BorrowedBytes => { + match self.ty.family() { + Family::Direct => ArgPass::Direct { slot: single() }, + Family::String => ArgPass::String { slot: single() }, + Family::Bytes => { let (ptr, len) = pair(); ArgPass::Bytes { ptr, len } } - TypeRef::Interface(_) => ArgPass::Object { + Family::Buffer => { + let (ptr, len) = pair(); + ArgPass::Buffer { ptr, len } + } + Family::Object { nullable } => ArgPass::Object { slot: single(), - nullable: false, + nullable, }, - // Only `Interface?` reaches here; every other optional is - // buffered. - TypeRef::Optional(_) => ArgPass::Object { - slot: single(), - nullable: true, - }, - _ => ArgPass::Direct { slot: single() }, + Family::Iterator => unreachable!("iterators are never parameters"), } } } +/// The runtime release a consumer wrapper owes for a producer-allocated +/// value it has copied or decoded. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Free { + /// By-value: nothing to free. + None, + /// A `const char*`: release with `{prefix}_free_string`. + String, + /// A `ptr` + `len` allocation (raw bytes or a value buffer): release with + /// `{prefix}_free_bytes(ptr, len)`. + Bytes, +} + /// How a callable's result crosses back to the wrapper: the receiving /// contract, including the decode step and the release obligation. /// -/// This is [`ReturnFree`] completed with the *decode* dimension: a bytes -/// return and a buffered return share a free obligation but differ in what -/// the wrapper does before freeing (copy versus decode). Only the sync and -/// async call shapes consult this; an iterator's result contract lives in -/// [`IteratorProtocol`]. +/// Only the sync and async call shapes consult this; an iterator's result +/// contract lives in [`IteratorProtocol`]. #[derive(Debug, Clone, PartialEq, Eq)] pub enum RetPass { /// No return value. @@ -184,13 +188,13 @@ pub enum RetPass { /// nothing to free. Direct, /// Owned `const char*`: copy into the native string, then release with - /// `{runtime}_free_string`. + /// `{prefix}_free_string`. String, /// Owned `(const uint8_t*, out_len)` raw bytes: copy, then release with - /// `{runtime}_free_bytes`. + /// `{prefix}_free_bytes`. Bytes, /// Owned `(const uint8_t*, out_len)` value buffer: decode via the wire - /// format ([`crate::wire`]), then release with `{runtime}_free_bytes`. + /// format ([`Ty::wire`]), then release with `{prefix}_free_bytes`. Buffer, /// An owned object reference the wrapper adopts into its disposal idiom. /// When `nullable`, the IDL type is `Interface?` and a null return means @@ -204,6 +208,19 @@ pub enum RetPass { }, } +impl RetPass { + /// The runtime release owed after copying or decoding the result. + /// Adopted objects owe their `destroy` symbol instead (see + /// [`RetPass::Object`]), so they report [`Free::None`] here. + pub fn free(&self) -> Free { + match self { + RetPass::String => Free::String, + RetPass::Bytes | RetPass::Buffer => Free::Bytes, + RetPass::Void | RetPass::Direct | RetPass::Object { .. } => Free::None, + } + } +} + /// The receiving contract for a value of type `ty` returned from a callable /// declared inside `module` under `prefix`. `None` (a void return) is /// [`RetPass::Void`]. @@ -213,119 +230,44 @@ pub enum RetPass { /// Panics on an iterator return, whose contract is [`IteratorProtocol`], not /// a value-passing plan; backends dispatch on /// [`CallShape`](crate::model::CallShape) before consulting this. -pub fn ret_pass(ty: Option<&TypeRef>, module: &str, prefix: &str) -> RetPass { +pub fn ret_pass(ty: Option<&Ty>, module: &str, prefix: &str) -> RetPass { let Some(ty) = ty else { return RetPass::Void; }; - if is_buffered(ty) { - return RetPass::Buffer; - } - match ty { - TypeRef::StringUtf8 | TypeRef::BorrowedStr => RetPass::String, - TypeRef::Bytes | TypeRef::BorrowedBytes => RetPass::Bytes, - TypeRef::Interface(name) => RetPass::Object { - destroy_symbol: destroy_symbol(name, module, prefix), - nullable: false, + match ty.family() { + Family::Direct => RetPass::Direct, + Family::String => RetPass::String, + Family::Bytes => RetPass::Bytes, + Family::Buffer => RetPass::Buffer, + Family::Object { nullable } => RetPass::Object { + destroy_symbol: destroy_symbol( + ty.interface_name() + .expect("object family names an interface"), + module, + prefix, + ), + nullable, }, - // Only `Interface?` reaches here (every other optional is buffered). - TypeRef::Optional(inner) => { - let TypeRef::Interface(name) = inner.as_ref() else { - unreachable!("only optional interfaces escape buffering") - }; - RetPass::Object { - destroy_symbol: destroy_symbol(name, module, prefix), - nullable: true, - } - } - TypeRef::Iterator(_) => { - panic!("iterator returns follow IteratorProtocol, not a RetPass") - } - _ => RetPass::Direct, + Family::Iterator => panic!("iterator returns follow IteratorProtocol, not a RetPass"), } } /// The release call a consumer wrapper owes for one *element* it copied out -/// of an iterator `next` slot. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ElemFree { - /// By-value element (scalar, bool, C-style enum, handle): nothing to free. - None, - /// A `const char*` element: release with `{runtime}_free_string`. - String, - /// A `ptr` + `len` element (bytes, or any buffered value the wrapper - /// decodes): copy or decode, then `{runtime}_free_bytes(ptr, len)`. - Bytes, -} - -/// The release call a consumer wrapper owes after copying a *returned* -/// value into a native one. -/// -/// This is the single statement of the ownership contract the producer runtime -/// implements (`weaveffi-abi`'s lowering helpers): strings via -/// `{runtime}_free_string`, byte and value buffers via `{runtime}_free_bytes`, -/// owned interface objects via their `_destroy` symbol. A backend renders -/// these as its disposal calls (or wraps the object and defers the release to -/// its finalizer idiom). -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ReturnFree { - /// By-value return: nothing to free. - None, - /// `const char*`: copy, then `{runtime}_free_string(ptr)`. - String, - /// `const uint8_t* + out_len`: copy (bytes) or decode (a buffered value), - /// then `{runtime}_free_bytes(ptr, len)`. - Bytes, - /// An owned interface object: the caller owns the reference and - /// eventually calls `destroy_symbol`. Wrappers adopt the pointer into - /// their disposal idiom (RAII, `__del__`, finalizers, `close()`), rather - /// than freeing eagerly. - OwnedObject { - /// The `{prefix}_{module}_{Name}_destroy` symbol to call. - destroy_symbol: String, - }, -} +/// of an iterator `next` slot. This is [`Free`] under its historical name. +pub type ElemFree = Free; /// The per-element release owed for one iterator element of type `ty`. -pub fn elem_free(ty: &TypeRef) -> ElemFree { - if is_buffered(ty) { - return ElemFree::Bytes; - } - match ty { - TypeRef::StringUtf8 | TypeRef::BorrowedStr => ElemFree::String, - TypeRef::Bytes | TypeRef::BorrowedBytes => ElemFree::Bytes, - _ => ElemFree::None, - } -} - -/// The release plan for a value of type `ty` *returned* from a callable -/// declared inside `module` under `prefix`. `None` (a void return) owes -/// nothing. -pub fn return_free(ty: Option<&TypeRef>, module: &str, prefix: &str) -> ReturnFree { - let Some(ty) = ty else { - return ReturnFree::None; - }; - if is_buffered(ty) { - return ReturnFree::Bytes; - } - match ty { - TypeRef::StringUtf8 | TypeRef::BorrowedStr => ReturnFree::String, - TypeRef::Bytes | TypeRef::BorrowedBytes => ReturnFree::Bytes, - TypeRef::Interface(name) => ReturnFree::OwnedObject { - destroy_symbol: destroy_symbol(name, module, prefix), - }, - // Only `Interface?` reaches here (every other optional is buffered): - // a nullable owned object pointer, null meaning none. - TypeRef::Optional(inner) => return_free(Some(inner), module, prefix), - // The iterator handle's lifecycle is the iterator protocol's own - // destroy symbol (see `IteratorProtocol`), not a buffer release. - TypeRef::Iterator(_) => ReturnFree::None, - _ => ReturnFree::None, +pub fn elem_free(ty: &Ty) -> ElemFree { + match ty.family() { + Family::String => Free::String, + Family::Bytes | Family::Buffer => Free::Bytes, + _ => Free::None, } } /// The `{prefix}_{module}_{Name}_destroy` symbol for a (possibly /// dot-qualified) interface name referenced from `current_module`. -fn destroy_symbol(name: &str, current_module: &str, prefix: &str) -> String { +pub fn destroy_symbol(name: &str, current_module: &str, prefix: &str) -> String { let (module, name) = split_qualified(name, current_module); format!("{prefix}_{module}_{name}_destroy") } @@ -409,37 +351,21 @@ pub struct AsyncProtocol<'a> { /// Whether the launcher carries a `cancel_token` slot before /// `callback`/`context`. pub cancellable: bool, - /// The release owed for an *owned interface* result adopted by the - /// callback; [`ReturnFree::None`] for results copied or decoded and - /// then released through the runtime free symbols. - pub result_adopt: ReturnFree, + /// How the callback's result slots are received, including the release + /// owed ([`RetPass::free`]) or the destroy symbol an adopted object owes. + pub result: RetPass, /// How the callback's `err` slot is interpreted. pub error: ErrorStrategy, } impl AsyncBinding { /// Build the full completion contract for this async function, resolving - /// the result-adoption plan against the declaring `module` and `prefix`. - /// - /// A direct or optional interface result (where an optional's null slot - /// simply means none) is adopted by the callback; every other result - /// shape is copied or decoded, then freed through the runtime symbols. + /// the result plan against the declaring `module` and `prefix`. pub fn protocol<'a>(&'a self, f: &FnBinding, module: &str, prefix: &str) -> AsyncProtocol<'a> { - fn adoptable(ty: &TypeRef) -> Option<&TypeRef> { - match ty { - TypeRef::Interface(_) => Some(ty), - TypeRef::Optional(inner) => adoptable(inner), - _ => None, - } - } - let result_adopt = match f.ret.as_ref().and_then(|ty| adoptable(ty)) { - Some(ty) => return_free(Some(ty), module, prefix), - None => ReturnFree::None, - }; AsyncProtocol { binding: self, cancellable: f.cancellable, - result_adopt, + result: ret_pass(f.ret.as_ref(), module, prefix), error: f.error_strategy(), } } @@ -449,101 +375,35 @@ impl AsyncBinding { mod tests { use super::*; - #[test] - fn strings_and_bytes_have_runtime_frees() { - assert_eq!( - return_free(Some(&TypeRef::StringUtf8), "m", "weaveffi"), - ReturnFree::String - ); - assert_eq!( - return_free(Some(&TypeRef::Bytes), "m", "weaveffi"), - ReturnFree::Bytes - ); - assert_eq!(return_free(None, "m", "weaveffi"), ReturnFree::None); - } - - #[test] - fn buffered_returns_are_freed_as_bytes() { - for ty in [ - TypeRef::Record("Contact".into()), - TypeRef::RichEnum("Shape".into()), - TypeRef::List(Box::new(TypeRef::StringUtf8)), - TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - TypeRef::Optional(Box::new(TypeRef::I64)), - TypeRef::Optional(Box::new(TypeRef::Record("Contact".into()))), - ] { - assert_eq!( - return_free(Some(&ty), "m", "weaveffi"), - ReturnFree::Bytes, - "{ty:?}" - ); - } - } - - #[test] - fn interface_returns_are_adopted_with_destroy_symbols() { - assert_eq!( - return_free( - Some(&TypeRef::Interface("kv.Store".into())), - "kv_stats", - "weaveffi" - ), - ReturnFree::OwnedObject { - destroy_symbol: "weaveffi_kv_Store_destroy".into() - } - ); - // A nullable interface shares the plan; null means none. - assert_eq!( - return_free( - Some(&TypeRef::Optional(Box::new(TypeRef::Interface( - "Store".into() - )))), - "kv", - "weaveffi" - ), - ReturnFree::OwnedObject { - destroy_symbol: "weaveffi_kv_Store_destroy".into() - } - ); - } - #[test] fn arg_pass_classifies_every_family() { - use crate::abi::lower_param; - let pb = |name: &str, ty: TypeRef| ParamBinding { - abi: lower_param(name, &ty, "m", false), - name: name.into(), - ty, - mutable: false, - doc: None, - }; - + let pb = |name: &str, ty: Ty| ParamBinding::new(name, ty, false, None, "m"); assert!(matches!( - pb("x", TypeRef::I32).arg_pass(), + pb("x", Ty::I32).arg_pass(), ArgPass::Direct { slot } if slot.name == "x" )); assert!(matches!( - pb("h", TypeRef::Handle).arg_pass(), + pb("h", Ty::Handle).arg_pass(), ArgPass::Direct { .. } )); assert!(matches!( - pb("s", TypeRef::StringUtf8).arg_pass(), + pb("s", Ty::StringUtf8).arg_pass(), ArgPass::String { slot } if slot.name == "s" )); assert!(matches!( - pb("data", TypeRef::Bytes).arg_pass(), + pb("data", Ty::Bytes).arg_pass(), ArgPass::Bytes { ptr, len } if ptr.name == "data_ptr" && len.name == "data_len" )); assert!(matches!( - pb("c", TypeRef::Record("Contact".into())).arg_pass(), + pb("c", Ty::Record("Contact".into())).arg_pass(), ArgPass::Buffer { ptr, len } if ptr.name == "c_ptr" && len.name == "c_len" )); assert!(matches!( - pb("o", TypeRef::Optional(Box::new(TypeRef::I32))).arg_pass(), + pb("o", Ty::Optional(Box::new(Ty::I32))).arg_pass(), ArgPass::Buffer { .. } )); assert!(matches!( - pb("store", TypeRef::Interface("Store".into())).arg_pass(), + pb("store", Ty::Interface("Store".into())).arg_pass(), ArgPass::Object { nullable: false, .. @@ -552,7 +412,7 @@ mod tests { assert!(matches!( pb( "store", - TypeRef::Optional(Box::new(TypeRef::Interface("Store".into()))) + Ty::Optional(Box::new(Ty::Interface("Store".into()))) ) .arg_pass(), ArgPass::Object { nullable: true, .. } @@ -562,28 +422,26 @@ mod tests { #[test] fn ret_pass_distinguishes_copy_decode_and_adopt() { assert_eq!(ret_pass(None, "m", "weaveffi"), RetPass::Void); + assert_eq!(ret_pass(Some(&Ty::I64), "m", "weaveffi"), RetPass::Direct); assert_eq!( - ret_pass(Some(&TypeRef::I64), "m", "weaveffi"), - RetPass::Direct - ); - assert_eq!( - ret_pass(Some(&TypeRef::StringUtf8), "m", "weaveffi"), + ret_pass(Some(&Ty::StringUtf8), "m", "weaveffi"), RetPass::String ); + assert_eq!(ret_pass(Some(&Ty::Bytes), "m", "weaveffi"), RetPass::Bytes); + for ty in [ + Ty::Record("Contact".into()), + Ty::RichEnum("Shape".into()), + Ty::List(Box::new(Ty::StringUtf8)), + Ty::Optional(Box::new(Ty::I64)), + ] { + assert_eq!( + ret_pass(Some(&ty), "m", "weaveffi"), + RetPass::Buffer, + "{ty}" + ); + } assert_eq!( - ret_pass(Some(&TypeRef::Bytes), "m", "weaveffi"), - RetPass::Bytes - ); - assert_eq!( - ret_pass(Some(&TypeRef::Record("Contact".into())), "m", "weaveffi"), - RetPass::Buffer - ); - assert_eq!( - ret_pass( - Some(&TypeRef::Interface("kv.Store".into())), - "m", - "weaveffi" - ), + ret_pass(Some(&Ty::Interface("kv.Store".into())), "m", "weaveffi"), RetPass::Object { destroy_symbol: "weaveffi_kv_Store_destroy".into(), nullable: false, @@ -591,9 +449,7 @@ mod tests { ); assert_eq!( ret_pass( - Some(&TypeRef::Optional(Box::new(TypeRef::Interface( - "Store".into() - )))), + Some(&Ty::Optional(Box::new(Ty::Interface("Store".into())))), "kv", "weaveffi" ), @@ -602,17 +458,17 @@ mod tests { nullable: true, } ); + assert_eq!(RetPass::String.free(), Free::String); + assert_eq!(RetPass::Buffer.free(), Free::Bytes); + assert_eq!(RetPass::Direct.free(), Free::None); } #[test] fn iterator_elements_split_string_bytes_and_none() { - assert_eq!(elem_free(&TypeRef::I32), ElemFree::None); - assert_eq!(elem_free(&TypeRef::StringUtf8), ElemFree::String); - assert_eq!(elem_free(&TypeRef::Bytes), ElemFree::Bytes); - assert_eq!(elem_free(&TypeRef::Record("Entry".into())), ElemFree::Bytes); - assert_eq!( - elem_free(&TypeRef::Optional(Box::new(TypeRef::I32))), - ElemFree::Bytes - ); + assert_eq!(elem_free(&Ty::I32), Free::None); + assert_eq!(elem_free(&Ty::StringUtf8), Free::String); + assert_eq!(elem_free(&Ty::Bytes), Free::Bytes); + assert_eq!(elem_free(&Ty::Record("Entry".into())), Free::Bytes); + assert_eq!(elem_free(&Ty::Optional(Box::new(Ty::I32))), Free::Bytes); } } diff --git a/crates/weaveffi-core/src/resolved.rs b/crates/weaveffi-core/src/resolved.rs index 051a7d1e..51c4dbe6 100644 --- a/crates/weaveffi-core/src/resolved.rs +++ b/crates/weaveffi-core/src/resolved.rs @@ -1,74 +1,215 @@ -//! The resolved IR: a proof-carrying wrapper around a validated [`Api`]. +//! The resolved IR: a validated [`Api`] paired with the type index that turns +//! its written [`TypeRef`]s into resolved [`Ty`]s. //! -//! WeaveFFI's pipeline has two distinct representations of an API that used -//! to share one mutable type: +//! WeaveFFI keeps two distinct representations of an API: //! //! * the **IDL document**: the [`Api`] tree exactly as parsed from //! YAML/JSON/TOML (or extracted from annotated Rust), in which every -//! user-type reference is still an unresolved [`TypeRef::Named`]; and -//! * the **resolved IR**: the same tree after [`validate_api`] has checked -//! every rule and rewritten each `Named` reference into its resolved kind -//! ([`TypeRef::Record`], [`TypeRef::RichEnum`], [`TypeRef::Enum`], or -//! [`TypeRef::Interface`]), qualifying cross-module references. +//! user-type reference is a [`TypeRef::Named`] string; and +//! * the **binding model** ([`crate::model::BindingModel`]): the lowered view +//! generators consume, in which every type is a [`Ty`] whose kind (record, +//! enum, interface) and owning module are known. //! -//! [`ResolvedApi`] is the second representation, made unforgeable: the only -//! checked way to obtain one is [`validate_api`], and the wrapper exposes no -//! mutable access, so a generator holding a `&ResolvedApi` can rely on the -//! post-resolution invariants (no `Named` reference remains anywhere in the -//! tree) instead of documenting them as a convention. -//! -//! Everything downstream of validation consumes this type: the -//! [`BindingModel`](crate::model::BindingModel), the -//! [`Generator`](crate::codegen::Generator) and -//! [`LanguageBackend`](crate::backend::LanguageBackend) traits, and the -//! [`Orchestrator`](crate::codegen::Orchestrator). Code that manipulates the -//! *document* (parsing, `weaveffi format`, `weaveffi extract`) keeps working -//! with the plain [`Api`]. -//! -//! [`validate_api`]: crate::validate::validate_api -//! [`TypeRef::Named`]: weaveffi_ir::ir::TypeRef::Named -//! [`TypeRef::Record`]: weaveffi_ir::ir::TypeRef::Record -//! [`TypeRef::RichEnum`]: weaveffi_ir::ir::TypeRef::RichEnum -//! [`TypeRef::Enum`]: weaveffi_ir::ir::TypeRef::Enum -//! [`TypeRef::Interface`]: weaveffi_ir::ir::TypeRef::Interface +//! [`ResolvedApi`] is the bridge. The only checked way to obtain one is +//! [`validate_api`](crate::validate::validate_api), which proves every rule +//! holds; [`ResolvedApi::resolve`] then maps a written reference to its +//! resolved type against the index built from the document's declarations. +//! The document itself is never mutated, so an IDL always round-trips. + +use std::collections::BTreeMap; -use weaveffi_ir::ir::Api; +use weaveffi_ir::ir::{Api, Module, TypeRef}; + +use crate::model::Ty; +use crate::pkg::Package; + +/// What kind of declaration a bare type name refers to. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TypeKind { + /// A `structs:` entry. + Record, + /// An `enums:` entry with no payload-carrying variant. + Enum, + /// An `enums:` entry with at least one payload-carrying variant. + RichEnum, + /// An `interfaces:` entry. + Interface, +} -/// A validated API whose type references have all been resolved. +/// Where a type is declared: the owning module's dot-joined path and its kind. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TypeDecl { + /// Dot-joined path of the declaring module (e.g. `graphics.shapes`). + pub module_path: String, + /// The declaration kind. + pub kind: TypeKind, +} + +/// A validated API whose type references can be resolved. +/// +/// This is the input to [`BindingModel::build`](crate::model::BindingModel::build). +/// It cannot be constructed from an unchecked document except through +/// [`assume_valid`](Self::assume_valid), which exists for the two construction +/// sites that cannot route through validation: the `#[weaveffi::module]` +/// proc-macro (which expands one module in isolation) and tests that build +/// well-formed trees by hand. /// -/// This is the input every generator consumes. It cannot be constructed -/// from an unchecked document: obtain one through -/// [`validate_api`](crate::validate::validate_api), or through -/// [`assume_resolved`](Self::assume_resolved) when resolution has provably -/// happened by other means (the producer macro's expansion path, or tests -/// that build already-resolved trees by hand). +/// `ResolvedApi` dereferences to [`Api`] for read access; there is no mutable +/// access, so the index can never go stale. /// -/// `ResolvedApi` dereferences to [`Api`] for read access; there is no -/// mutable access, so the resolution invariants cannot be broken after -/// construction. +/// The CLI also attaches the project's [`Package`] identity (from the +/// `[package]` table of `weaveffi.toml`) with [`with_package`](Self::with_package), +/// so every backend resolves manifest metadata from one place. #[derive(Debug, Clone, PartialEq)] pub struct ResolvedApi { api: Api, + types: BTreeMap, + package: Option, } impl ResolvedApi { - /// Wrap an [`Api`] the caller asserts is already validated and resolved. + /// Attach the project's package identity. Generators read it through + /// [`pkg::resolve`](crate::pkg::resolve). + #[must_use] + pub fn with_package(mut self, package: Package) -> Self { + self.package = Some(package); + self + } + + /// The project's package identity, when the CLI attached one. + pub fn package(&self) -> Option<&Package> { + self.package.as_ref() + } + + /// Wrap an [`Api`] the caller asserts satisfies every validation rule, + /// indexing its declarations for [`resolve`](Self::resolve). /// - /// This is the escape hatch for the two construction sites that cannot - /// route through [`validate_api`](crate::validate::validate_api): the - /// `#[weaveffi::module]` proc-macro (which resolves the single module it - /// expands) and tests that build pre-resolved trees directly. The caller - /// asserts that no [`TypeRef::Named`](weaveffi_ir::ir::TypeRef::Named) - /// reference remains anywhere in `api`; downstream lowering treats a - /// surviving one as a bug and panics on it. - pub fn assume_resolved(api: Api) -> Self { - Self { api } - } - - /// Borrow the underlying resolved [`Api`] tree. + /// A reference to a name the document does not declare resolves as a + /// [`Ty::Record`]. That fallback exists for the proc-macro, which expands + /// a single `#[weaveffi::module]` and may legitimately reference a value + /// type declared in a sibling module it cannot see; a record and a rich + /// enum share the same buffered ABI shape, so the emitted thunk is correct + /// for both, and `rustc` rejects a genuinely undefined type. Documents + /// that went through [`validate_api`](crate::validate::validate_api) + /// never hit the fallback. + pub fn assume_valid(api: Api) -> Self { + let mut types = BTreeMap::new(); + for module in &api.modules { + index_module(module, "", &mut types); + } + Self { + api, + types, + package: None, + } + } + + /// Borrow the underlying [`Api`] document. pub fn api(&self) -> &Api { &self.api } + + /// Look up where a bare type name is declared. + pub fn declaration(&self, name: &str) -> Option<&TypeDecl> { + self.types.get(name) + } + + /// Resolve a written type reference against the declarations in scope + /// from the module at `module_path` (dot-joined, e.g. `kv.stats`). + /// + /// User references become their declared kind. A reference to a type + /// declared in a *different* module is qualified with the owner's + /// dot-joined path (`shared.Status`), so the ABI lowering emits the + /// owner's symbol prefix; a reference to a type in the same module stays + /// bare. Typed handles are qualified the same way. + pub fn resolve(&self, ty: &TypeRef, module_path: &str) -> Ty { + match ty { + TypeRef::I8 => Ty::I8, + TypeRef::I16 => Ty::I16, + TypeRef::I32 => Ty::I32, + TypeRef::I64 => Ty::I64, + TypeRef::U8 => Ty::U8, + TypeRef::U16 => Ty::U16, + TypeRef::U32 => Ty::U32, + TypeRef::U64 => Ty::U64, + TypeRef::F32 => Ty::F32, + TypeRef::F64 => Ty::F64, + TypeRef::Bool => Ty::Bool, + TypeRef::StringUtf8 => Ty::StringUtf8, + TypeRef::Bytes => Ty::Bytes, + TypeRef::BorrowedStr => Ty::BorrowedStr, + TypeRef::BorrowedBytes => Ty::BorrowedBytes, + TypeRef::Handle => Ty::Handle, + TypeRef::TypedHandle(name) => Ty::TypedHandle(self.qualify(name, module_path)), + TypeRef::Named(name) => { + let qualified = self.qualify(name, module_path); + match self.types.get(bare(name)).map(|d| d.kind) { + Some(TypeKind::Enum) => Ty::Enum(qualified), + Some(TypeKind::RichEnum) => Ty::RichEnum(qualified), + Some(TypeKind::Interface) => Ty::Interface(qualified), + Some(TypeKind::Record) | None => Ty::Record(qualified), + } + } + TypeRef::Optional(inner) => Ty::Optional(Box::new(self.resolve(inner, module_path))), + TypeRef::List(inner) => Ty::List(Box::new(self.resolve(inner, module_path))), + TypeRef::Map(k, v) => Ty::Map( + Box::new(self.resolve(k, module_path)), + Box::new(self.resolve(v, module_path)), + ), + TypeRef::Iterator(inner) => Ty::Iterator(Box::new(self.resolve(inner, module_path))), + } + } + + /// Qualify `name` with its owner's module path when the owner is not the + /// module at `module_path`. An already-qualified name is normalized to the + /// owner's canonical path. + fn qualify(&self, name: &str, module_path: &str) -> String { + let bare_name = bare(name); + match self.types.get(bare_name) { + Some(decl) if decl.module_path == module_path => bare_name.to_string(), + Some(decl) => format!("{}.{bare_name}", decl.module_path), + None => name.to_string(), + } + } +} + +/// The final segment of a possibly dot-qualified name. +fn bare(name: &str) -> &str { + name.rsplit('.').next().unwrap_or(name) +} + +/// Recursively index every struct/enum/interface in the module tree by bare +/// name. Bare names are globally unique (validation enforces it), so the map +/// has one entry per name; the first declaration wins otherwise. +fn index_module(module: &Module, parent: &str, out: &mut BTreeMap) { + let path = if parent.is_empty() { + module.name.clone() + } else { + format!("{parent}.{}", module.name) + }; + let mut add = |name: &str, kind: TypeKind| { + out.entry(name.to_string()).or_insert(TypeDecl { + module_path: path.clone(), + kind, + }); + }; + for s in &module.structs { + add(&s.name, TypeKind::Record); + } + for e in &module.enums { + let kind = if e.is_rich() { + TypeKind::RichEnum + } else { + TypeKind::Enum + }; + add(&e.name, kind); + } + for i in &module.interfaces { + add(&i.name, TypeKind::Interface); + } + for child in &module.modules { + index_module(child, &path, out); + } } impl std::ops::Deref for ResolvedApi { @@ -84,3 +225,142 @@ impl AsRef for ResolvedApi { &self.api } } + +#[cfg(test)] +mod tests { + use super::*; + use weaveffi_ir::ir::{ + EnumDef, EnumVariant, InterfaceDef, StructDef, StructField, CURRENT_SCHEMA_VERSION, + }; + + fn module(name: &str) -> Module { + Module { + name: name.into(), + doc: None, + functions: vec![], + interfaces: vec![], + structs: vec![], + enums: vec![], + callbacks: vec![], + listeners: vec![], + errors: None, + modules: vec![], + } + } + + fn api() -> ResolvedApi { + let shared = Module { + enums: vec![ + EnumDef { + name: "Status".into(), + doc: None, + deprecated: None, + variants: vec![EnumVariant { + name: "Ok".into(), + value: 0, + doc: None, + fields: vec![], + }], + }, + EnumDef { + name: "Shape".into(), + doc: None, + deprecated: None, + variants: vec![EnumVariant { + name: "Circle".into(), + value: 0, + doc: None, + fields: vec![StructField { + name: "r".into(), + ty: TypeRef::F64, + doc: None, + }], + }], + }, + ], + structs: vec![StructDef { + name: "Point".into(), + doc: None, + deprecated: None, + fields: vec![], + }], + modules: vec![Module { + interfaces: vec![InterfaceDef { + name: "Store".into(), + doc: None, + deprecated: None, + constructors: vec![], + methods: vec![], + statics: vec![], + }], + ..module("inner") + }], + ..module("shared") + }; + ResolvedApi::assume_valid(Api { + version: CURRENT_SCHEMA_VERSION.into(), + modules: vec![shared, module("orders")], + }) + } + + #[test] + fn kinds_and_qualification() { + let api = api(); + assert_eq!( + api.resolve(&TypeRef::Named("Status".into()), "shared"), + Ty::Enum("Status".into()) + ); + assert_eq!( + api.resolve(&TypeRef::Named("Status".into()), "orders"), + Ty::Enum("shared.Status".into()) + ); + assert_eq!( + api.resolve(&TypeRef::Named("Shape".into()), "orders"), + Ty::RichEnum("shared.Shape".into()) + ); + assert_eq!( + api.resolve(&TypeRef::Named("Store".into()), "shared"), + Ty::Interface("shared.inner.Store".into()) + ); + assert_eq!( + api.resolve(&TypeRef::Named("Store".into()), "shared.inner"), + Ty::Interface("Store".into()) + ); + // Already-qualified spellings normalize to the owner's path. + assert_eq!( + api.resolve(&TypeRef::Named("shared.Point".into()), "orders"), + Ty::Record("shared.Point".into()) + ); + assert_eq!( + api.resolve(&TypeRef::TypedHandle("Store".into()), "orders"), + Ty::TypedHandle("shared.inner.Store".into()) + ); + } + + #[test] + fn composites_resolve_recursively() { + let api = api(); + let ty = TypeRef::List(Box::new(TypeRef::Map( + Box::new(TypeRef::StringUtf8), + Box::new(TypeRef::Optional(Box::new(TypeRef::Named("Point".into())))), + ))); + assert_eq!( + api.resolve(&ty, "orders"), + Ty::List(Box::new(Ty::Map( + Box::new(Ty::StringUtf8), + Box::new(Ty::Optional(Box::new(Ty::Record("shared.Point".into())))) + ))) + ); + } + + #[test] + fn unknown_names_fall_back_to_records() { + let api = api(); + assert_eq!( + api.resolve(&TypeRef::Named("Elsewhere".into()), "orders"), + Ty::Record("Elsewhere".into()) + ); + assert!(api.declaration("Elsewhere").is_none()); + assert_eq!(api.declaration("Point").unwrap().kind, TypeKind::Record); + } +} diff --git a/crates/weaveffi-core/src/utils.rs b/crates/weaveffi-core/src/utils.rs index 1a5c27a1..30f6415f 100644 --- a/crates/weaveffi-core/src/utils.rs +++ b/crates/weaveffi-core/src/utils.rs @@ -89,7 +89,9 @@ pub fn render_json_prelude(input_basename: &str) -> String { pub const ABI_RUNTIME_SYMBOLS: &[&str] = &[ "error", "handle_t", + "abi_version", "error_clear", + "error_free", "free_string", "free_bytes", "arena_create", diff --git a/crates/weaveffi-core/src/validate.rs b/crates/weaveffi-core/src/validate.rs index 376c7035..7e2e43d5 100644 --- a/crates/weaveffi-core/src/validate.rs +++ b/crates/weaveffi-core/src/validate.rs @@ -1,7 +1,7 @@ //! IDL validation. This module owns the [`ValidationError`] catalog and the //! [`validate_api`] entry point; the work is split across submodules: -//! `rules` (per-module checks), `resolve` (type-reference qualification), -//! `diagnostic` (miette span attachment), and `warnings` (advisory lints). +//! `rules` (per-module checks), `diagnostic` (miette span attachment), and +//! `warnings` (advisory lints). //! //! Validation collects *every* rule violation before failing, so a document //! with several problems reports them all in one run rather than one per @@ -14,14 +14,12 @@ use weaveffi_ir::ir::{Api, SUPPORTED_VERSIONS}; use crate::resolved::ResolvedApi; mod diagnostic; -mod resolve; mod rules; #[cfg(test)] mod tests; mod warnings; pub use diagnostic::ValidationDiagnostic; -pub use resolve::{find_type_in_api, resolve_type_refs}; pub use warnings::{collect_warnings, ValidationWarning}; /// Every way an [`Api`] can fail validation. @@ -534,10 +532,10 @@ impl Diagnostic for ValidationDiagnostics { /// on-disk source. /// /// This is the one checked way to turn a parsed document into the -/// [`ResolvedApi`] every generator consumes. On success, type references are -/// resolved (see [`resolve_type_refs`]): enum and interface references are -/// distinguished from struct references, and cross-module references are -/// qualified with their owning module's path. +/// [`ResolvedApi`] every generator consumes. The document is never rewritten; +/// the resolved view indexes its declarations so that +/// [`ResolvedApi::resolve`] can turn each written reference into a +/// [`Ty`](crate::model::Ty) whose kind and owning module are known. /// /// # Errors /// @@ -546,12 +544,12 @@ impl Diagnostic for ValidationDiagnostics { /// an unknown or misplaced type, an empty struct or enum, a `throws` without /// an error domain, or any other rule violation in the catalog above. pub fn validate_api( - mut api: Api, + api: Api, source: Option<(&str, &str)>, ) -> Result { - let errors = validate_api_inner(&mut api); + let errors = validate_api_inner(&api); if errors.is_empty() { - return Ok(ResolvedApi::assume_resolved(api)); + return Ok(ResolvedApi::assume_valid(api)); } Err(ValidationDiagnostics { diagnostics: errors @@ -561,7 +559,7 @@ pub fn validate_api( }) } -fn validate_api_inner(api: &mut Api) -> Vec { +fn validate_api_inner(api: &Api) -> Vec { let mut errors = Vec::new(); if !SUPPORTED_VERSIONS.contains(&api.version.as_str()) { // A wrong-schema document is checked no further: the rules below @@ -571,17 +569,15 @@ fn validate_api_inner(api: &mut Api) -> Vec { supported: SUPPORTED_VERSIONS.join(", "), }]; } + let types = rules::TypeIndex::build(&api.modules); let mut module_names = BTreeSet::new(); for m in &api.modules { if !module_names.insert(m.name.clone()) { errors.push(ValidationError::DuplicateModuleName(m.name.clone())); } - rules::validate_module(m, &api.modules, false, &mut errors); + rules::validate_module(m, &types, false, &mut errors); } rules::check_global_type_names(&api.modules, &mut errors); rules::check_global_error_code_names(&api.modules, &mut errors); - if errors.is_empty() { - resolve_type_refs(api); - } errors } diff --git a/crates/weaveffi-core/src/validate/resolve.rs b/crates/weaveffi-core/src/validate/resolve.rs deleted file mode 100644 index c78d7ce1..00000000 --- a/crates/weaveffi-core/src/validate/resolve.rs +++ /dev/null @@ -1,229 +0,0 @@ -//! Type-reference resolution: rewrites parsed [`TypeRef::Named`] references -//! into their resolved kinds ([`TypeRef::Record`], [`TypeRef::RichEnum`], -//! [`TypeRef::Enum`], or [`TypeRef::Interface`]) and qualifies cross-module -//! references with the owning module's dot-joined path. Runs after the rule -//! checks pass, so every name is known to resolve. - -use std::collections::BTreeMap; -use weaveffi_ir::ir::{Api, Function, Module, TypeRef}; - -/// How a bare type name resolves: a record, a C-style enum, an algebraic -/// (rich) enum, or an interface. The kinds differ in their C ABI lowering: a -/// C-style enum is a by-value integer ([`TypeRef::Enum`]), a record and a rich -/// enum are opaque object pointers ([`TypeRef::Record`] / -/// [`TypeRef::RichEnum`]), and an interface is an opaque object reference with -/// its own ownership convention ([`TypeRef::Interface`]). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum TypeKind { - Record, - Enum, - RichEnum, - Interface, -} - -impl TypeKind { - /// Build the resolved [`TypeRef`] for this kind with the given (possibly - /// module-qualified) name. - fn type_ref(self, name: String) -> TypeRef { - match self { - TypeKind::Record => TypeRef::Record(name), - TypeKind::Enum => TypeRef::Enum(name), - TypeKind::RichEnum => TypeRef::RichEnum(name), - TypeKind::Interface => TypeRef::Interface(name), - } - } -} - -/// Resolve every type reference in `api` in place. -/// -/// Every [`TypeRef::Named`] is rewritten to the variant matching its -/// declaration: [`TypeRef::Record`] for structs, [`TypeRef::RichEnum`] for -/// algebraic enums, [`TypeRef::Enum`] for C-style enums, and -/// [`TypeRef::Interface`] for interfaces. Cross-module references are -/// qualified with the owning module's dot-joined path. Runs after the rule -/// checks pass, so no unresolved `Named` reference remains afterwards. -pub fn resolve_type_refs(api: &mut Api) { - let mut global_types: BTreeMap = BTreeMap::new(); - for module in &api.modules { - index_module_types(module, "", &mut global_types); - } - - for module in &mut api.modules { - resolve_module_type_refs(module, "", &global_types); - } -} - -/// Recursively index every struct/enum/interface in the module tree by its -/// bare name, recording the owner's *dot-joined* module path (e.g. -/// `graphics.shapes`) so cross-module and nested references can be -/// auto-qualified. Bare names are globally unique (validation enforces it), -/// so the map has one entry per name. -fn index_module_types( - module: &Module, - parent_path: &str, - out: &mut BTreeMap, -) { - let path = join_module_path(parent_path, &module.name); - for s in &module.structs { - out.entry(s.name.clone()) - .or_insert((path.clone(), TypeKind::Record)); - } - for e in &module.enums { - let kind = if e.is_rich() { - TypeKind::RichEnum - } else { - TypeKind::Enum - }; - out.entry(e.name.clone()).or_insert((path.clone(), kind)); - } - for i in &module.interfaces { - out.entry(i.name.clone()) - .or_insert((path.clone(), TypeKind::Interface)); - } - for child in &module.modules { - index_module_types(child, &path, out); - } -} - -/// Resolve every type reference inside `module` (and recursively its -/// submodules), qualifying cross-module references against `global_types`. -/// `current_module` is tracked as a dot-joined path so multi-level nesting -/// resolves correctly. -fn resolve_module_type_refs( - module: &mut Module, - parent_path: &str, - global_types: &BTreeMap, -) { - let module_path = join_module_path(parent_path, &module.name); - let ctx = ResolveCtx { - current_module: &module_path, - global_types, - }; - fn resolve_callable(f: &mut Function, ctx: &ResolveCtx<'_>) { - for p in &mut f.params { - resolve_single_type_ref(&mut p.ty, ctx); - } - if let Some(ret) = &mut f.returns { - resolve_single_type_ref(ret, ctx); - } - } - for f in &mut module.functions { - resolve_callable(f, &ctx); - } - for i in &mut module.interfaces { - for c in &mut i.constructors { - resolve_callable(c, &ctx); - } - for m in &mut i.methods { - resolve_callable(m, &ctx); - } - for s in &mut i.statics { - resolve_callable(s, &ctx); - } - } - for s in &mut module.structs { - for field in &mut s.fields { - resolve_single_type_ref(&mut field.ty, &ctx); - } - } - // A rich enum's variant fields are themselves type references that must be - // resolved (e.g. a variant field of struct or sibling-enum type). - for e in &mut module.enums { - for v in &mut e.variants { - for field in &mut v.fields { - resolve_single_type_ref(&mut field.ty, &ctx); - } - } - } - for cb in &mut module.callbacks { - for p in &mut cb.params { - resolve_single_type_ref(&mut p.ty, &ctx); - } - } - for child in &mut module.modules { - resolve_module_type_refs(child, &module_path, global_types); - } -} - -/// Bundled lookup tables for resolving a single type reference within one -/// module, so the recursive resolver does not thread several parameters. -struct ResolveCtx<'a> { - current_module: &'a str, - global_types: &'a BTreeMap, -} - -/// Join a parent module path with a child segment using `.`. A top-level -/// module (empty parent) is just its own name, preserving the single-segment -/// behavior that existed before nested resolution. -fn join_module_path(parent_path: &str, name: &str) -> String { - if parent_path.is_empty() { - name.to_string() - } else { - format!("{parent_path}.{name}") - } -} - -fn resolve_single_type_ref(ty: &mut TypeRef, ctx: &ResolveCtx<'_>) { - match ty { - // A parsed bare name resolves against the global type index; a - // reference declared outside the current module is additionally - // qualified with the owner's dot-joined path so the C ABI lowering - // and every generator emit the owner's symbol prefix. - TypeRef::Named(name) => { - if let Some((mod_name, kind)) = ctx.global_types.get(name.as_str()) { - let resolved_name = if mod_name == ctx.current_module { - std::mem::take(name) - } else { - format!("{mod_name}.{name}") - }; - *ty = kind.type_ref(resolved_name); - } - } - // A typed handle's target may live in a different module (e.g. - // `handle` in a `kv.stats` submodule referring to `kv.Store`); - // qualify it to the owner's path so the C ABI lowering emits the - // owner's symbol prefix rather than the referrer's. - TypeRef::TypedHandle(name) => { - if let Some((mod_name, _kind)) = ctx.global_types.get(name.as_str()) { - if mod_name != ctx.current_module { - *name = format!("{mod_name}.{name}"); - } - } - } - TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => { - resolve_single_type_ref(inner, ctx); - } - TypeRef::Map(k, v) => { - resolve_single_type_ref(k, ctx); - resolve_single_type_ref(v, ctx); - } - _ => {} - } -} - -/// Locate a struct, enum, or interface by its bare `name` anywhere in `api`. -/// -/// Returns the owning module's dot-joined path and a boolean that is `true` -/// when the match is an enum and `false` when it is a struct or interface, or -/// `None` if no type with that name exists. Bare names are globally unique -/// (validation enforces it), so at most one declaration can match. -pub fn find_type_in_api(api: &Api, name: &str) -> Option<(String, bool)> { - fn search(module: &Module, parent_path: &str, name: &str) -> Option<(String, bool)> { - let path = join_module_path(parent_path, &module.name); - if module.structs.iter().any(|s| s.name == name) - || module.interfaces.iter().any(|i| i.name == name) - { - return Some((path, false)); - } - if module.enums.iter().any(|e| e.name == name) { - return Some((path, true)); - } - module - .modules - .iter() - .find_map(|child| search(child, &path, name)) - } - api.modules - .iter() - .find_map(|module| search(module, "", name)) -} diff --git a/crates/weaveffi-core/src/validate/rules.rs b/crates/weaveffi-core/src/validate/rules.rs index 7ed5e11d..2c1963c8 100644 --- a/crates/weaveffi-core/src/validate/rules.rs +++ b/crates/weaveffi-core/src/validate/rules.rs @@ -8,47 +8,7 @@ use super::ValidationError; use std::collections::{BTreeMap, BTreeSet}; -use weaveffi_ir::ir::{ErrorDomain, Function, InterfaceDef, Module, Param, TypeRef}; - -/// Whether a type may appear as a callback parameter. Callback arguments -/// cross the boundary *into* the foreign language, either as a direct slot -/// (scalars, strings, bytes, handles) or as a borrowed serialized buffer -/// (records, rich enums, optionals, lists, maps), so nearly everything is -/// marshalable. The exceptions: iterators have no callback protocol, and a -/// borrowed view can appear only as the parameter's own type (a buffer -/// cannot hold a borrow). Interfaces are rejected separately. -fn callback_param_type_supported(ty: &TypeRef) -> bool { - if contains_iterator(ty) { - return false; - } - match ty { - TypeRef::BorrowedStr | TypeRef::BorrowedBytes => true, - _ => contains_borrowed(ty).is_none(), - } -} - -/// The types allowed as map keys: scalars, bool, enums, and strings. Every -/// target language must be able to use the key in its native dictionary -/// idiom, so composites, optionals, bytes, and handles are rejected. -fn scalar_element(ty: &TypeRef) -> bool { - matches!( - ty, - TypeRef::I8 - | TypeRef::I16 - | TypeRef::I32 - | TypeRef::U8 - | TypeRef::U16 - | TypeRef::U32 - | TypeRef::I64 - | TypeRef::U64 - | TypeRef::F32 - | TypeRef::F64 - | TypeRef::Bool - | TypeRef::Enum(_) - | TypeRef::StringUtf8 - | TypeRef::BorrowedStr - ) -} +use weaveffi_ir::ir::{ErrorDomain, Function, InterfaceDef, Module, Param, StructField, TypeRef}; const RESERVED: &[&str] = &[ "if", "else", "for", "while", "loop", "match", "type", "return", "async", "await", "break", @@ -79,6 +39,87 @@ fn check_identifier(name: &str, errors: &mut Vec) -> bool { true } +/// The kinds of declarations a bare name can refer to, gathered from the +/// whole module forest before any rule runs. Validation operates on the +/// document as written (every user reference is a [`TypeRef::Named`]), so +/// positional rules consult this index to learn what a name is. +#[derive(Default)] +pub(super) struct TypeIndex { + /// Bare names of every struct, enum, and interface anywhere in the API. + pub all: BTreeSet, + /// Bare names of every interface anywhere in the API. + pub interfaces: BTreeSet, + /// Bare names of every C-style (payload-free) enum anywhere in the API. + pub plain_enums: BTreeSet, +} + +impl TypeIndex { + pub(super) fn build(modules: &[Module]) -> Self { + let mut index = Self::default(); + index.walk(modules); + index + } + + fn walk(&mut self, modules: &[Module]) { + for m in modules { + for s in &m.structs { + self.all.insert(s.name.clone()); + } + for e in &m.enums { + self.all.insert(e.name.clone()); + if !e.is_rich() { + self.plain_enums.insert(e.name.clone()); + } + } + for i in &m.interfaces { + self.all.insert(i.name.clone()); + self.interfaces.insert(i.name.clone()); + } + self.walk(&m.modules); + } + } + + /// Is `name` (bare or dot-qualified) an interface? + fn is_interface(&self, name: &str) -> bool { + self.interfaces.contains(bare(name)) + } + + /// Does a struct, enum, or interface with this (bare or dot-qualified) + /// name exist anywhere in the API? + fn exists(&self, name: &str) -> bool { + self.all.contains(bare(name)) + } + + /// May `ty` be a map key? Every target must be able to use the key in + /// its native dictionary idiom, so only scalars, bools, strings, and + /// C-style enums qualify; composites, optionals, bytes, and handles are + /// rejected. + fn is_map_key(&self, ty: &TypeRef) -> bool { + match ty { + TypeRef::I8 + | TypeRef::I16 + | TypeRef::I32 + | TypeRef::U8 + | TypeRef::U16 + | TypeRef::U32 + | TypeRef::I64 + | TypeRef::U64 + | TypeRef::F32 + | TypeRef::F64 + | TypeRef::Bool + | TypeRef::StringUtf8 + | TypeRef::BorrowedStr => true, + TypeRef::Named(name) => self.plain_enums.contains(bare(name)), + _ => false, + } + } +} + +/// The final segment of a possibly dot-qualified name. +fn bare(name: &str) -> &str { + name.rsplit('.').next().unwrap_or(name) +} + /// Enforce global bare-name uniqueness for the type namespace: structs, /// enums, interfaces, and error domains across every module (including /// nested submodules). @@ -163,22 +204,9 @@ pub(super) fn check_global_error_code_names(modules: &[Module], errors: &mut Vec walk(modules, "", &mut seen, errors); } -/// Collect the bare names of every interface declared anywhere in the module -/// tree. Validation runs before reference resolution, so an interface -/// reference is still spelled `TypeRef::Named(name)` and positional rules -/// need this set to recognize one. -pub(super) fn collect_interface_names(modules: &[Module], out: &mut BTreeSet) { - for m in modules { - for i in &m.interfaces { - out.insert(i.name.clone()); - } - collect_interface_names(&m.modules, out); - } -} - pub(super) fn validate_module( module: &Module, - all_modules: &[Module], + types: &TypeIndex, ancestor_has_domain: bool, errors: &mut Vec, ) { @@ -198,16 +226,14 @@ pub(super) fn validate_module( )); } - let mut interface_names = BTreeSet::new(); - collect_interface_names(all_modules, &mut interface_names); let has_domain = ancestor_has_domain || module.errors.is_some(); // Every C symbol suffix a module-level callable claims: free functions, // interface members, and implicit destructors. Two entries with the same // suffix would produce two identical C symbols. - let mut symbol_suffixes: BTreeMap = BTreeMap::new(); + let mut symbol_suffixes: BTreeSet = BTreeSet::new(); let mut claim_symbol = |suffix: String, errors: &mut Vec| { - if symbol_suffixes.insert(suffix.clone(), ()).is_some() { + if !symbol_suffixes.insert(suffix.clone()) { errors.push(ValidationError::AbiSymbolCollision { module: module.name.clone(), symbol: suffix, @@ -312,38 +338,25 @@ pub(super) fn validate_module( validate_interface(module, i, has_domain, &mut claim_symbol, errors); } - let known_types: BTreeSet<&str> = struct_names - .iter() - .map(|s| s.as_str()) - .chain(enum_names.iter().map(|s| s.as_str())) - .chain(local_interface_names.iter().map(|s| s.as_str())) - .collect(); - let ctx = TypeCtx { - known: &known_types, - all_modules, - interfaces: &interface_names, - }; - // A field of a record, a rich-enum variant, or an error payload is // serialized inside a value buffer, so it obeys the buffered positional // rules: no borrowed views, no iterators, no interfaces, and every // reference must resolve. - let mut check_buffered_field = - |f: &weaveffi_ir::ir::StructField, location: &dyn Fn() -> String| { - if let Some(ty) = contains_borrowed(&f.ty) { - errors.push(ValidationError::BorrowedTypeInInvalidPosition { - ty: ty.to_string(), - location: location(), - }); - } - if contains_iterator(&f.ty) { - errors.push(ValidationError::IteratorInInvalidPosition { - location: location(), - }); - } - validate_type_ref(&f.ty, &ctx, errors); - check_interface_positions(&f.ty, &ctx, false, location, errors); - }; + let mut check_buffered_field = |f: &StructField, location: &dyn Fn() -> String| { + if let Some(ty) = contains_borrowed(&f.ty) { + errors.push(ValidationError::BorrowedTypeInInvalidPosition { + ty: ty.to_string(), + location: location(), + }); + } + if contains_iterator(&f.ty) { + errors.push(ValidationError::IteratorInInvalidPosition { + location: location(), + }); + } + validate_type_ref(&f.ty, types, errors); + check_interface_positions(&f.ty, types, false, location, errors); + }; for s in &module.structs { for f in &s.fields { let location = || format!("field '{}' of struct '{}'", f.name, s.name); @@ -372,20 +385,12 @@ pub(super) fn validate_module( } } for f in &module.functions { - validate_callable_types(&module.name, &f.name, f, &ctx, errors); + validate_callable_types(&module.name, &f.name, f, types, errors); } for i in &module.interfaces { - for c in &i.constructors { - let display = format!("{}.{}", i.name, c.name); - validate_callable_types(&module.name, &display, c, &ctx, errors); - } - for m in &i.methods { - let display = format!("{}.{}", i.name, m.name); - validate_callable_types(&module.name, &display, m, &ctx, errors); - } - for s in &i.statics { - let display = format!("{}.{}", i.name, s.name); - validate_callable_types(&module.name, &display, s, &ctx, errors); + for f in i.constructors.iter().chain(&i.methods).chain(&i.statics) { + let display = format!("{}.{}", i.name, f.name); + validate_callable_types(&module.name, &display, f, types, errors); } } @@ -400,9 +405,10 @@ pub(super) fn validate_module( } for p in &cb.params { validate_param(p, errors); - if references_interface(&p.ty, &ctx) { + validate_type_ref(&p.ty, types, errors); + if references_interface(&p.ty, types) { errors.push(ValidationError::InterfaceInInvalidPosition { - name: type_ref_display(&p.ty), + name: user_name_in(&p.ty), location: format!("param '{}' of callback '{}'", p.name, cb.name), }); } else if !callback_param_type_supported(&p.ty) { @@ -410,7 +416,7 @@ pub(super) fn validate_module( module: module.name.clone(), callback: cb.name.clone(), param: p.name.clone(), - ty: format!("{:?}", p.ty), + ty: p.ty.to_string(), }); } } @@ -443,7 +449,7 @@ pub(super) fn validate_module( if !sub_module_names.insert(sub.name.clone()) { errors.push(ValidationError::DuplicateModuleName(sub.name.clone())); } - validate_module(sub, all_modules, has_domain, errors); + validate_module(sub, types, has_domain, errors); } } @@ -536,7 +542,7 @@ fn validate_callable_types( module_name: &str, display_name: &str, f: &Function, - ctx: &TypeCtx<'_>, + types: &TypeIndex, errors: &mut Vec, ) { for p in &f.params { @@ -577,11 +583,11 @@ fn validate_callable_types( errors.push(ValidationError::MutableParamUnsupported { function: format!("{module_name}::{display_name}"), param: p.name.clone(), - ty: format!("{:?}", p.ty), + ty: p.ty.to_string(), }); } - validate_type_ref(&p.ty, ctx, errors); - check_interface_positions(&p.ty, ctx, true, location, errors); + validate_type_ref(&p.ty, types, errors); + check_interface_positions(&p.ty, types, true, location, errors); } if let Some(ret) = &f.returns { let location = || format!("return type of {module_name}::{display_name}"); @@ -601,8 +607,8 @@ fn validate_callable_types( function: display_name.to_string(), }); } - validate_type_ref(ret, ctx, errors); - check_interface_positions(ret, ctx, true, location, errors); + validate_type_ref(ret, types, errors); + check_interface_positions(ret, types, true, location, errors); } } @@ -610,48 +616,45 @@ fn validate_param(p: &Param, errors: &mut Vec) { check_identifier(&p.name, errors); } -/// Shared context for type-reference checks: the current module's own type -/// names, the full module forest for cross-module lookups, and the global set -/// of interface names. -struct TypeCtx<'a> { - known: &'a BTreeSet<&'a str>, - all_modules: &'a [Module], - interfaces: &'a BTreeSet, -} - -impl TypeCtx<'_> { - /// Is `name` (bare or dot-qualified) an interface? - fn is_interface(&self, name: &str) -> bool { - let bare = name.rsplit('.').next().unwrap_or(name); - self.interfaces.contains(bare) +/// Whether a type may appear as a callback parameter. Callback arguments +/// cross the boundary *into* the foreign language, either as a direct slot +/// (scalars, strings, bytes, handles) or as a borrowed serialized buffer +/// (records, rich enums, optionals, lists, maps), so nearly everything is +/// marshalable. The exceptions: iterators have no callback protocol, and a +/// borrowed view can appear only as the parameter's own type (a buffer +/// cannot hold a borrow). Interfaces are rejected separately. +fn callback_param_type_supported(ty: &TypeRef) -> bool { + if contains_iterator(ty) { + return false; + } + match ty { + TypeRef::BorrowedStr | TypeRef::BorrowedBytes => true, + _ => contains_borrowed(ty).is_none(), } } /// Does `ty` reference an interface anywhere in its structure? Used for /// positions where interfaces are wholly disallowed (callback parameters). -fn references_interface(ty: &TypeRef, ctx: &TypeCtx<'_>) -> bool { +fn references_interface(ty: &TypeRef, types: &TypeIndex) -> bool { match ty { - TypeRef::Named(name) | TypeRef::Interface(name) => ctx.is_interface(name), + TypeRef::Named(name) => types.is_interface(name), TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => { - references_interface(inner, ctx) + references_interface(inner, types) } - TypeRef::Map(k, v) => references_interface(k, ctx) || references_interface(v, ctx), + TypeRef::Map(k, v) => references_interface(k, types) || references_interface(v, types), _ => false, } } -/// Render the interface name inside `ty` for an error message, falling back -/// to the debug spelling for composite shapes. -fn type_ref_display(ty: &TypeRef) -> String { +/// The innermost user-type name inside `ty` for an error message, falling +/// back to the IDL spelling for shapes without one. +fn user_name_in(ty: &TypeRef) -> String { match ty { - TypeRef::Named(name) - | TypeRef::Record(name) - | TypeRef::RichEnum(name) - | TypeRef::Interface(name) => name.clone(), + TypeRef::Named(name) => name.clone(), TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => { - type_ref_display(inner) + user_name_in(inner) } - other => format!("{other:?}"), + other => other.to_string(), } } @@ -663,14 +666,14 @@ fn type_ref_display(ty: &TypeRef) -> String { /// copies the object cannot provide. fn check_interface_positions( ty: &TypeRef, - ctx: &TypeCtx<'_>, + types: &TypeIndex, top: bool, location: impl Fn() -> String + Copy, errors: &mut Vec, ) { match ty { - TypeRef::Named(name) | TypeRef::Interface(name) => { - if ctx.is_interface(name) && !top { + TypeRef::Named(name) => { + if types.is_interface(name) && !top { errors.push(ValidationError::InterfaceInInvalidPosition { name: name.clone(), location: location(), @@ -680,7 +683,7 @@ fn check_interface_positions( // A typed handle names a token *type tag*, not an object; pointing // one at an interface would conflate u64 tokens with object pointers. TypeRef::TypedHandle(name) => { - if ctx.is_interface(name) { + if types.is_interface(name) { errors.push(ValidationError::InterfaceInInvalidPosition { name: name.clone(), location: format!("typed handle in {}", location()), @@ -689,13 +692,13 @@ fn check_interface_positions( } // Optionality does not change the position: `Store?` is still a // top-level object reference. - TypeRef::Optional(inner) => check_interface_positions(inner, ctx, top, location, errors), + TypeRef::Optional(inner) => check_interface_positions(inner, types, top, location, errors), TypeRef::List(inner) | TypeRef::Iterator(inner) => { - check_interface_positions(inner, ctx, false, location, errors); + check_interface_positions(inner, types, false, location, errors); } TypeRef::Map(k, v) => { - check_interface_positions(k, ctx, false, location, errors); - check_interface_positions(v, ctx, false, location, errors); + check_interface_positions(k, types, false, location, errors); + check_interface_positions(v, types, false, location, errors); } _ => {} } @@ -722,49 +725,24 @@ fn contains_iterator(ty: &TypeRef) -> bool { } } -/// Does a struct, enum, or interface named `name` exist anywhere in the -/// module tree, including nested submodules? Validation runs before reference -/// qualification, so an unqualified reference is valid if its bare name is -/// defined anywhere; the resolver later rewrites it to the owning module's -/// full path. -fn type_exists(modules: &[Module], name: &str) -> bool { - 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) - || type_exists(&m.modules, name) - }) -} - -fn validate_type_ref(ty: &TypeRef, ctx: &TypeCtx<'_>, errors: &mut Vec) { +fn validate_type_ref(ty: &TypeRef, types: &TypeIndex, errors: &mut Vec) { match ty { - TypeRef::Named(name) - | TypeRef::Record(name) - | TypeRef::RichEnum(name) - | TypeRef::Interface(name) - | TypeRef::Enum(name) - | TypeRef::TypedHandle(name) => { - if !ctx.known.contains(name.as_str()) && !type_exists(ctx.all_modules, name) { + TypeRef::Named(name) | TypeRef::TypedHandle(name) => { + if !types.exists(name) { errors.push(ValidationError::UnknownTypeRef { name: name.clone() }); } } TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => { - validate_type_ref(inner, ctx, errors); + validate_type_ref(inner, types, errors); } TypeRef::Map(k, v) => { - if !scalar_element(k) { - let key_type = match k.as_ref() { - TypeRef::Named(name) | TypeRef::Record(name) | TypeRef::RichEnum(name) => { - format!("struct {name}") - } - TypeRef::List(_) => "list".to_string(), - TypeRef::Map(_, _) => "map".to_string(), - other => format!("{other:?}"), - }; - errors.push(ValidationError::InvalidMapKey { key_type }); + if !types.is_map_key(k) { + errors.push(ValidationError::InvalidMapKey { + key_type: k.to_string(), + }); } - validate_type_ref(k, ctx, errors); - validate_type_ref(v, ctx, errors); + validate_type_ref(k, types, errors); + validate_type_ref(v, types, errors); } _ => {} } diff --git a/crates/weaveffi-core/src/validate/tests.rs b/crates/weaveffi-core/src/validate/tests.rs index 520df909..0082aab7 100644 --- a/crates/weaveffi-core/src/validate/tests.rs +++ b/crates/weaveffi-core/src/validate/tests.rs @@ -1,3295 +1,650 @@ -//! The validator's test suite. Fixtures are built in memory (no on-disk -//! IDL), so spans/snippets are exercised separately via the CLI tests. +//! The validator's test suite. +//! +//! Every case is a short YAML document paired with the error variant it must +//! (or must not) produce, so adding a rule means adding one table row and a +//! few lines of IDL rather than forty lines of struct literals. Spans and +//! snippets are exercised by the CLI tests against on-disk files. use super::*; -use weaveffi_ir::ir::{ - Api, CallbackDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, ListenerDef, Module, - Param, StructDef, StructField, TypeRef, -}; +use weaveffi_ir::ir::CURRENT_SCHEMA_VERSION; +use weaveffi_ir::parse::parse_api_str; -fn simple_function(name: &str) -> Function { - Function { - name: name.to_string(), - params: vec![Param { - name: "x".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - } +/// Parse a YAML body (without the `version:` line) and validate it. +fn check(body: &str) -> Result { + let doc = format!("version: \"{CURRENT_SCHEMA_VERSION}\"\n{body}"); + let api = parse_api_str(&doc, "yaml").expect("fixture must parse"); + validate_api(api, None) } -fn simple_module(name: &str) -> Module { - Module { - name: name.to_string(), - functions: vec![simple_function("do_stuff")], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], +/// Variant discriminator names of every error the document produced. +fn errors(body: &str) -> Vec { + match check(body) { + Ok(_) => vec![], + Err(d) => d.diagnostics.iter().map(|d| variant(&d.error)).collect(), } } -fn simple_api() -> Api { - Api { - version: "0.7.0".to_string(), - modules: vec![simple_module("mymod")], - generators: None, - package: None, +/// The variant name of a [`ValidationError`] (the debug spelling up to the +/// first `(` or ` {`). +fn variant(e: &ValidationError) -> String { + let dbg = format!("{e:?}"); + dbg.split(['(', ' ']).next().unwrap_or(&dbg).to_string() +} + +const MATH: &str = r#" +modules: + - name: math + functions: + - name: add + params: [{ name: a, type: i32 }, { name: b, type: i32 }] + return: i32 +"#; + +#[test] +fn valid_documents_pass() { + let ok: &[&str] = &[ + MATH, + r#" +modules: + - name: shared + enums: + - name: Status + variants: [{ name: Ok, value: 0 }, { name: Bad, value: 1 }] + - name: Shape + variants: + - { name: Dot, value: 0 } + - { name: Circle, value: 1, fields: [{ name: r, type: f64 }] } + structs: + - name: Point + fields: + - { name: x, type: f64 } + - { name: status, type: Status } + - { name: shape, type: "Shape?" } + - { name: tags, type: "[string]" } + - { 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 }] } + interfaces: + - name: Store + constructors: [{ name: open, params: [{ name: path, type: "&str" }] }] + methods: + - { name: get, params: [{ name: k, type: string }], return: "Point?", throws: true } + - { name: scan, params: [], return: "iter" } + - { name: sibling, params: [{ name: other, type: "Store?" }], return: Store } + statics: [{ name: version, params: [], return: string }] + callbacks: + - name: on_point + params: [{ name: p, type: Point }, { name: raw, type: "&[u8]" }] + listeners: + - { name: points, event_callback: on_point } + functions: + - { name: fetch, params: [{ name: id, type: i64 }], return: "[Point]", async: true, cancellable: true } + - { name: token, params: [], return: "handle" } + - { name: rename, params: [{ name: s, type: string, mutable: true }] } + modules: + - name: inner + functions: + - { name: fails, params: [], throws: true } +"#, + ]; + for doc in ok { + assert_eq!(errors(doc), Vec::::new(), "{doc}"); } } #[test] -fn valid_api_passes() { - let api = simple_api(); - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn duplicate_module_names_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![simple_module("dup"), simple_module("dup")], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::DuplicateModuleName(n) if n == "dup" - )); -} - -#[test] -fn duplicate_function_names_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("same"), simple_function("same")], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::DuplicateFunctionName { .. } - )); -} - -#[test] -fn reserved_keywords_rejected() { - for kw in ["type", "async"] { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: kw.to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!( - validate_api(api, None).is_err(), - "Expected reserved keyword '{kw}' to be rejected" - ); - } -} - -#[test] -fn invalid_identifiers_rejected() { - for bad in ["123", "has spaces", ""] { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: bad.to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; +fn every_rule_fires() { + let cases: &[(&str, &str)] = &[ + ( + "DuplicateModuleName", + "modules: [{ name: a, functions: [] }, { name: a, functions: [] }]", + ), + ( + "NoModuleName", + "modules: [{ name: ' ', functions: [] }]", + ), + ( + "InvalidModuleName", + "modules: [{ name: 9lives, functions: [] }]", + ), + ( + "InvalidModuleName", + "modules: [{ name: async, functions: [] }]", + ), + ( + "DuplicateFunctionName", + r#" +modules: + - name: m + functions: + - { name: f, params: [] } + - { name: f, params: [] } +"#, + ), + ( + "DuplicateParamName", + r#" +modules: + - name: m + functions: + - { name: f, params: [{ name: x, type: i32 }, { name: x, type: i32 }] } +"#, + ), + ( + "ReservedKeyword", + "modules: [{ name: m, functions: [{ name: match, params: [] }] }]", + ), + ( + "InvalidIdentifier", + "modules: [{ name: m, functions: [{ name: 'has space', params: [] }] }]", + ), + ( + "ErrorDomainMissingName", + r#" +modules: + - name: m + errors: { name: ' ', codes: [] } +"#, + ), + ( + "DuplicateErrorName", + r#" +modules: + - name: m + errors: + name: E + codes: + - { name: A, code: 1, message: a } + - { name: A, code: 2, message: a } +"#, + ), + ( + "DuplicateErrorCode", + r#" +modules: + - name: m + errors: + name: E + codes: + - { name: A, code: 1, message: a } + - { name: B, code: 1, message: b } +"#, + ), + ( + "InvalidErrorCode", + r#" +modules: + - name: m + errors: + name: E + codes: [{ name: A, code: 0, message: a }] +"#, + ), + ( + "InvalidErrorCode", + r#" +modules: + - name: m + errors: + name: E + codes: [{ name: A, code: -2, message: a }] +"#, + ), + ( + "NameCollisionWithErrorDomain", + r#" +modules: + - name: m + functions: [{ name: E, params: [] }] + errors: { name: E, codes: [{ name: A, code: 1, message: a }] } +"#, + ), + ( + "AbiSymbolCollision", + r#" +modules: + - name: m + functions: [{ name: Store_get, params: [] }] + interfaces: + - name: Store + methods: [{ name: get, params: [] }] +"#, + ), + ( + "AbiSymbolCollision", + r#" +modules: + - name: m + functions: [{ name: Store_destroy, params: [] }] + interfaces: + - name: Store + methods: [{ name: get, params: [] }] +"#, + ), + ( + "ThrowsWithoutErrorDomain", + "modules: [{ name: m, functions: [{ name: f, params: [], throws: true }] }]", + ), + ( + "DuplicateTypeName", + r#" +modules: + - name: a + structs: [{ name: Config, fields: [{ name: x, type: i32 }] }] + - name: b + enums: [{ name: Config, variants: [{ name: A, value: 0 }] }] +"#, + ), + ( + "DuplicateTypeName", + r#" +modules: + - name: a + structs: [{ name: E, fields: [{ name: x, type: i32 }] }] + errors: { name: E, codes: [{ name: A, code: 1, message: a }] } +"#, + ), + ( + "DuplicateErrorCodeName", + r#" +modules: + - name: a + errors: { name: AErr, codes: [{ name: NotFound, code: 1, message: a }] } + - name: b + errors: { name: BErr, codes: [{ name: NotFound, code: 1, message: b }] } +"#, + ), + ( + "DuplicateStructName", + r#" +modules: + - name: m + structs: + - { name: S, fields: [{ name: x, type: i32 }] } + - { name: S, fields: [{ name: x, type: i32 }] } +"#, + ), + ( + "DuplicateStructField", + r#" +modules: + - name: m + structs: + - { name: S, fields: [{ name: x, type: i32 }, { name: x, type: i32 }] } +"#, + ), + ( + "EmptyStruct", + "modules: [{ name: m, structs: [{ name: S, fields: [] }] }]", + ), + ( + "DuplicateEnumName", + r#" +modules: + - name: m + enums: + - { name: E, variants: [{ name: A, value: 0 }] } + - { name: E, variants: [{ name: A, value: 0 }] } +"#, + ), + ( + "EmptyEnum", + "modules: [{ name: m, enums: [{ name: E, variants: [] }] }]", + ), + ( + "DuplicateEnumVariant", + r#" +modules: + - name: m + enums: + - { name: E, variants: [{ name: A, value: 0 }, { name: A, value: 1 }] } +"#, + ), + ( + "DuplicateEnumVariantField", + r#" +modules: + - name: m + enums: + - name: E + variants: + - { name: A, value: 0, fields: [{ name: x, type: i32 }, { name: x, type: i32 }] } +"#, + ), + ( + "DuplicateEnumValue", + r#" +modules: + - name: m + enums: + - { name: E, variants: [{ name: A, value: 0 }, { name: B, value: 0 }] } +"#, + ), + ( + "DuplicateInterfaceName", + r#" +modules: + - name: m + interfaces: + - { name: I, methods: [{ name: a, params: [] }] } + - { name: I, methods: [{ name: b, params: [] }] } +"#, + ), + ( + "DuplicateInterfaceMember", + r#" +modules: + - name: m + interfaces: + - name: I + constructors: [{ name: a, params: [] }] + methods: [{ name: a, params: [] }] +"#, + ), + ( + "EmptyInterface", + "modules: [{ name: m, interfaces: [{ name: I }] }]", + ), + ( + "ConstructorHasReturn", + r#" +modules: + - name: m + interfaces: + - name: I + constructors: [{ name: new, params: [], return: i32 }] +"#, + ), + ( + "AsyncConstructor", + r#" +modules: + - name: m + interfaces: + - name: I + constructors: [{ name: new, params: [], async: true }] +"#, + ), + ( + "InterfaceInInvalidPosition", + r#" +modules: + - name: m + interfaces: [{ name: I, methods: [{ name: a, params: [] }] }] + structs: [{ name: S, fields: [{ name: i, type: I }] }] +"#, + ), + ( + "InterfaceInInvalidPosition", + r#" +modules: + - name: m + interfaces: [{ name: I, methods: [{ name: a, params: [] }] }] + functions: [{ name: f, params: [{ name: xs, type: "[I]" }] }] +"#, + ), + ( + "InterfaceInInvalidPosition", + r#" +modules: + - name: m + interfaces: [{ name: I, methods: [{ name: a, params: [] }] }] + functions: [{ name: f, params: [], return: "handle" }] +"#, + ), + ( + "InterfaceInInvalidPosition", + r#" +modules: + - name: m + interfaces: [{ name: I, methods: [{ name: a, params: [] }] }] + callbacks: [{ name: cb, params: [{ name: i, type: I }] }] +"#, + ), + ( + "UnknownTypeRef", + "modules: [{ name: m, functions: [{ name: f, params: [], return: Nope }] }]", + ), + ( + "UnknownTypeRef", + "modules: [{ name: m, functions: [{ name: f, params: [], return: 'handle' }] }]", + ), + ( + "InvalidMapKey", + r#" +modules: + - name: m + structs: [{ name: S, fields: [{ name: x, type: i32 }] }] + functions: [{ name: f, params: [], return: "{S:i32}" }] +"#, + ), + ( + "InvalidMapKey", + r#" +modules: + - name: m + enums: + - name: Rich + variants: [{ name: A, value: 0, fields: [{ name: x, type: i32 }] }] + functions: [{ name: f, params: [], return: "{Rich:i32}" }] +"#, + ), + ( + "InvalidMapKey", + "modules: [{ name: m, functions: [{ name: f, params: [], return: '{bytes:i32}' }] }]", + ), + ( + "BorrowedTypeInInvalidPosition", + "modules: [{ name: m, functions: [{ name: f, params: [], return: '&str' }] }]", + ), + ( + "BorrowedTypeInInvalidPosition", + "modules: [{ name: m, functions: [{ name: f, params: [{ name: x, type: '[&str]' }] }] }]", + ), + ( + "BorrowedTypeInInvalidPosition", + "modules: [{ name: m, structs: [{ name: S, fields: [{ name: x, type: '&[u8]' }] }] }]", + ), + ( + "DuplicateCallbackName", + r#" +modules: + - name: m + callbacks: [{ name: cb, params: [] }, { name: cb, params: [] }] +"#, + ), + ( + "ListenerCallbackNotFound", + "modules: [{ name: m, listeners: [{ name: l, event_callback: nope }] }]", + ), + ( + "DuplicateListenerName", + r#" +modules: + - name: m + callbacks: [{ name: cb, params: [] }] + listeners: [{ name: l, event_callback: cb }, { name: l, event_callback: cb }] +"#, + ), + ( + "UnsupportedCallbackParamType", + "modules: [{ name: m, callbacks: [{ name: cb, params: [{ name: x, type: 'iter' }] }] }]", + ), + ( + "UnsupportedCallbackParamType", + "modules: [{ name: m, callbacks: [{ name: cb, params: [{ name: x, type: '[&str]' }] }] }]", + ), + ( + "IteratorInInvalidPosition", + "modules: [{ name: m, functions: [{ name: f, params: [{ name: x, type: 'iter' }] }] }]", + ), + ( + "IteratorInInvalidPosition", + "modules: [{ name: m, structs: [{ name: S, fields: [{ name: x, type: 'iter' }] }] }]", + ), + ( + "MutableParamUnsupported", + "modules: [{ name: m, functions: [{ name: f, params: [{ name: x, type: i32, mutable: true }] }] }]", + ), + ( + "MutableParamUnsupported", + "modules: [{ name: m, functions: [{ name: f, params: [{ name: x, type: '[i32]', mutable: true }] }] }]", + ), + ( + "AsyncIteratorReturn", + "modules: [{ name: m, functions: [{ name: f, params: [], return: 'iter', async: true }] }]", + ), + ]; + for (expected, doc) in cases { + let got = errors(doc); assert!( - validate_api(api, None).is_err(), - "Expected invalid identifier '{bad}' to be rejected" + got.iter().any(|v| v == expected), + "expected {expected}, got {got:?} for:\n{doc}" ); } } #[test] -fn async_function_passes_validation() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "do_async".to_string(), - params: vec![], - returns: None, - doc: None, - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn async_function_with_return_passes() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "fetch_data".to_string(), - params: vec![Param { - name: "url".to_string(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn async_void_function_emits_warning() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "fire_and_forget".to_string(), - params: vec![], - returns: None, - doc: Some("documented".to_string()), - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let warnings = collect_warnings(&api); - assert!(warnings.iter().any(|w| matches!( - w, - ValidationWarning::AsyncVoidFunction { module, function } - if module == "mymod" && function == "fire_and_forget" - ))); -} - -#[test] -fn async_function_with_return_no_void_warning() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "fetch".to_string(), - params: vec![], - returns: Some(TypeRef::StringUtf8), - doc: Some("documented".to_string()), - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let warnings = collect_warnings(&api); - assert!(!warnings - .iter() - .any(|w| matches!(w, ValidationWarning::AsyncVoidFunction { .. }))); -} - -#[test] -fn empty_module_name_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::NoModuleName - )); -} - -#[test] -fn doc_example_error_domain_validates() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "contacts".to_string(), - functions: vec![ - Function { - name: "create_contact".to_string(), - params: vec![ - Param { - name: "name".to_string(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }, - Param { - name: "email".to_string(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::Handle), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "get_contact".to_string(), - params: vec![Param { - name: "id".to_string(), - ty: TypeRef::Handle, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: Some(ErrorDomain { - name: "ContactErrors".to_string(), - codes: vec![ - ErrorCode { - name: "not_found".to_string(), - code: 1, - message: "Contact not found".to_string(), - doc: None, - fields: vec![], - }, - ErrorCode { - name: "duplicate".to_string(), - code: 2, - message: "Contact already exists".to_string(), - doc: None, - fields: vec![], - }, - ErrorCode { - name: "invalid_email".to_string(), - code: 3, - message: "Email address is invalid".to_string(), - doc: None, - fields: vec![], - }, - ], - }), - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn error_code_zero_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: Some(ErrorDomain { - name: "MyErrors".to_string(), - codes: vec![ErrorCode { - name: "success".to_string(), - code: 0, - message: "should fail".to_string(), - doc: None, - fields: vec![], - }], - }), - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::InvalidErrorCode { module, name } - if module == "mymod" && name == "success" - )); -} - -#[test] -fn error_domain_name_collision_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("do_stuff")], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: Some(ErrorDomain { - name: "do_stuff".to_string(), - codes: vec![ErrorCode { - name: "fail".to_string(), - code: 1, - message: "failed".to_string(), - doc: None, - fields: vec![], - }], - }), - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::NameCollisionWithErrorDomain { module, name } - if module == "mymod" && name == "do_stuff" - )); -} - -#[test] -fn duplicate_error_names_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: Some(ErrorDomain { - name: "MyErrors".to_string(), - codes: vec![ - ErrorCode { - name: "fail".to_string(), - code: 1, - message: "failed".to_string(), - doc: None, - fields: vec![], - }, - ErrorCode { - name: "fail".to_string(), - code: 2, - message: "also failed".to_string(), - doc: None, - fields: vec![], - }, - ], - }), - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::DuplicateErrorName { module, name } - if module == "mymod" && name == "fail" - )); -} - -#[test] -fn duplicate_error_codes_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: Some(ErrorDomain { - name: "MyErrors".to_string(), - codes: vec![ - ErrorCode { - name: "not_found".to_string(), - code: 1, - message: "not found".to_string(), - doc: None, - fields: vec![], - }, - ErrorCode { - name: "timeout".to_string(), - code: 1, - message: "timed out".to_string(), - doc: None, - fields: vec![], - }, - ], - }), - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::DuplicateErrorCode { .. } - )); -} - -#[test] -fn duplicate_error_code_names_across_domains_rejected() { - let domain = |type_name: &str, code_name: &str| { - Some(ErrorDomain { - name: type_name.to_string(), - codes: vec![ErrorCode { - name: code_name.to_string(), - code: 1, - message: "gone".to_string(), - doc: None, - fields: vec![], - }], - }) - }; - let module = |name: &str, errors| Module { - name: name.to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors, - modules: vec![], - }; - let api = Api { - version: "0.7.0".to_string(), - modules: vec![ - module("products", domain("ProductsError", "NotFound")), - module("orders", domain("OrdersError", "NotFound")), - ], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::DuplicateErrorCodeName { name, first, second } - if name == "NotFound" - && first == "products.ProductsError" - && second == "orders.OrdersError" - )); - - // Distinct code names across domains stay valid. - let ok = Api { - version: "0.7.0".to_string(), - modules: vec![ - module("products", domain("ProductsError", "ProductNotFound")), - module("orders", domain("OrdersError", "OrderNotFound")), - ], - generators: None, - package: None, - }; - assert!(validate_api(ok, None).is_ok()); -} - -fn simple_struct(name: &str) -> StructDef { - StructDef { - name: name.to_string(), - doc: None, - fields: vec![StructField { - name: "x".to_string(), - ty: TypeRef::I32, - doc: None, - }], - } -} - -#[test] -fn duplicate_struct_names_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![simple_struct("Point"), simple_struct("Point")], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::DuplicateStructName { module, name } - if module == "mymod" && name == "Point" - )); -} - -#[test] -fn empty_struct_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![StructDef { - name: "Empty".to_string(), - doc: None, - fields: vec![], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::EmptyStruct { module, name } - if module == "mymod" && name == "Empty" - )); -} - -#[test] -fn duplicate_struct_field_names_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![StructDef { - name: "Point".to_string(), - doc: None, - fields: vec![ - StructField { - name: "x".to_string(), - ty: TypeRef::I32, - doc: None, - }, - StructField { - name: "x".to_string(), - ty: TypeRef::F64, - doc: None, - }, - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::DuplicateStructField { struct_name, field } - if struct_name == "Point" && field == "x" - )); -} - -fn simple_enum(name: &str) -> EnumDef { - EnumDef { - name: name.to_string(), - doc: None, - variants: vec![ - EnumVariant { - name: "A".to_string(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "B".to_string(), - value: 1, - doc: None, - fields: vec![], - }, - ], - } -} - -#[test] -fn duplicate_enum_names_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![], - enums: vec![simple_enum("Color"), simple_enum("Color")], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::DuplicateEnumName { module, name } - if module == "mymod" && name == "Color" - )); -} - -#[test] -fn empty_enum_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![], - enums: vec![EnumDef { - name: "Empty".to_string(), - doc: None, - variants: vec![], - }], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::EmptyEnum { module, name } - if module == "mymod" && name == "Empty" - )); -} - -#[test] -fn duplicate_enum_variant_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![], - enums: vec![EnumDef { - name: "Color".to_string(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".to_string(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Red".to_string(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::DuplicateEnumVariant { enum_name, variant } - if enum_name == "Color" && variant == "Red" - )); -} - -#[test] -fn duplicate_enum_value_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![], - enums: vec![EnumDef { - name: "Color".to_string(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".to_string(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".to_string(), - value: 0, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::DuplicateEnumValue { enum_name, value } - if enum_name == "Color" && *value == 0 - )); -} - -#[test] -fn unknown_type_ref_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "do_stuff".to_string(), - params: vec![Param { - name: "x".to_string(), - ty: TypeRef::Named("Foo".to_string()), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::UnknownTypeRef { name } if name == "Foo" - )); -} - -#[test] -fn valid_struct_ref_passes() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "do_stuff".to_string(), - params: vec![Param { - name: "p".to_string(), - ty: TypeRef::Named("Point".to_string()), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![simple_struct("Point")], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn unknown_type_ref_in_optional_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "do_stuff".to_string(), - params: vec![Param { - name: "x".to_string(), - ty: TypeRef::Optional(Box::new(TypeRef::Named("Bar".to_string()))), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::UnknownTypeRef { name } if name == "Bar" - )); -} - -#[test] -fn unknown_type_ref_in_list_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "do_stuff".to_string(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::Named("Baz".to_string())))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::UnknownTypeRef { name } if name == "Baz" - )); -} - -#[test] -fn struct_field_referencing_unknown_type() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![StructDef { - name: "Wrapper".to_string(), - doc: None, - fields: vec![StructField { - name: "inner".to_string(), - ty: TypeRef::Named("Nonexistent".to_string()), - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::UnknownTypeRef { name } if name == "Nonexistent" - )); -} - -#[test] -fn function_param_with_optional_struct() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "save".to_string(), - params: vec![Param { - name: "c".to_string(), - ty: TypeRef::Optional(Box::new(TypeRef::Named("Contact".to_string()))), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![StructDef { - name: "Contact".to_string(), - doc: None, - fields: vec![StructField { - name: "name".to_string(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn function_param_with_list_of_enums() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "paint".to_string(), - params: vec![Param { - name: "colors".to_string(), - ty: TypeRef::List(Box::new(TypeRef::Enum("Color".to_string()))), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![simple_enum("Color")], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn nested_optional_list_validates() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "list_contacts".to_string(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::Optional(Box::new( - TypeRef::Named("Contact".to_string()), - ))))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![StructDef { - name: "Contact".to_string(), - doc: None, - fields: vec![StructField { - name: "name".to_string(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn list_of_list_param_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "f".to_string(), - params: vec![Param { - name: "data".to_string(), - ty: TypeRef::List(Box::new(TypeRef::List(Box::new(TypeRef::I32)))), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - // Nested lists serialize recursively in the value-buffer format. - validate_api(api, None).expect("nested lists are representable as buffers"); -} - -#[test] -fn list_of_optional_scalar_return_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "f".to_string(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::Optional(Box::new( - TypeRef::I32, - ))))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - // The buffer format carries per-element presence flags, so a list of - // optional scalars round-trips. - validate_api(api, None).expect("optional scalars in lists are representable as buffers"); -} - -#[test] -fn map_of_struct_value_field_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![StructDef { - name: "Widget".to_string(), - doc: None, - fields: vec![StructField { - name: "parts".to_string(), - ty: TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::Named("Widget".to_string())), - ), - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - // Records nest recursively in the value-buffer format, including as map - // values inside another record's field. - validate_api(api, None).expect("maps of records are representable as buffers"); -} - -#[test] -fn enum_variant_value_zero_allowed() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![], - enums: vec![EnumDef { - name: "Status".to_string(), - doc: None, - variants: vec![ - EnumVariant { - name: "Unknown".to_string(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Active".to_string(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn valid_enum_ref_passes() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "get_color".to_string(), - params: vec![], - returns: Some(TypeRef::Enum("Color".to_string())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![simple_enum("Color")], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn resolve_enum_ref_in_function_param() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "paint".to_string(), - params: vec![Param { - name: "color".to_string(), - ty: TypeRef::Named("Color".to_string()), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![simple_enum("Color")], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let api = validate_api(api, None).unwrap(); - assert_eq!( - api.modules[0].functions[0].params[0].ty, - TypeRef::Enum("Color".to_string()) - ); -} - -#[test] -fn resolve_enum_ref_in_optional() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "paint".to_string(), - params: vec![Param { - name: "color".to_string(), - ty: TypeRef::Optional(Box::new(TypeRef::Named("Color".to_string()))), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![simple_enum("Color")], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let api = validate_api(api, None).unwrap(); - assert_eq!( - api.modules[0].functions[0].params[0].ty, - TypeRef::Optional(Box::new(TypeRef::Enum("Color".to_string()))) - ); -} - -#[test] -fn struct_ref_not_changed() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "save".to_string(), - params: vec![Param { - name: "c".to_string(), - ty: TypeRef::Named("Contact".to_string()), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![simple_struct("Contact")], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let api = validate_api(api, None).unwrap(); - assert_eq!( - api.modules[0].functions[0].params[0].ty, - TypeRef::Record("Contact".to_string()) - ); -} - -#[test] -fn map_with_string_key_passes() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "get_map".to_string(), - params: vec![], - returns: Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn map_with_struct_key_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "get_map".to_string(), - params: vec![], - returns: Some(TypeRef::Map( - Box::new(TypeRef::Named("Point".to_string())), - Box::new(TypeRef::I32), - )), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![simple_struct("Point")], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::InvalidMapKey { key_type } if key_type == "struct Point" - )); -} - -#[test] -fn map_with_enum_key_passes() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "get_map".to_string(), - params: vec![], - returns: Some(TypeRef::Map( - Box::new(TypeRef::Enum("Color".to_string())), - Box::new(TypeRef::StringUtf8), - )), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![simple_enum("Color")], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn warning_large_enum_variant_count() { - let variants: Vec = (0..101) - .map(|i| EnumVariant { - name: format!("V{i}"), - value: i, - doc: None, - fields: vec![], - }) - .collect(); - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![], - enums: vec![EnumDef { - name: "BigEnum".to_string(), - doc: None, - variants, - }], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let warnings = collect_warnings(&api); - assert!(warnings.iter().any(|w| matches!( - w, - ValidationWarning::LargeEnumVariantCount { enum_name, count } - if enum_name == "BigEnum" && *count == 101 - ))); -} - -#[test] -fn warning_enum_at_100_no_warning() { - let variants: Vec = (0..100) - .map(|i| EnumVariant { - name: format!("V{i}"), - value: i, - doc: None, - fields: vec![], - }) - .collect(); - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![], - enums: vec![EnumDef { - name: "BigEnum".to_string(), - doc: None, - variants, - }], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let warnings = collect_warnings(&api); - assert!(!warnings - .iter() - .any(|w| matches!(w, ValidationWarning::LargeEnumVariantCount { .. }))); -} - -#[test] -fn warning_deep_nesting_in_param() { - let deep = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional( - Box::new(TypeRef::List(Box::new(TypeRef::I32))), - ))))); - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "nested_fn".to_string(), - params: vec![Param { - name: "data".to_string(), - ty: deep, - mutable: false, - doc: None, - }], - returns: None, - doc: Some("documented".to_string()), - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let warnings = collect_warnings(&api); - assert!(warnings.iter().any(|w| matches!( - w, - ValidationWarning::DeepNesting { location, depth } - if location == "mymod::nested_fn::data" && *depth == 4 - ))); -} - -#[test] -fn warning_nesting_at_3_no_warning() { - let nested = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional( - Box::new(TypeRef::I32), - ))))); - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "ok_fn".to_string(), - params: vec![Param { - name: "data".to_string(), - ty: nested, - mutable: false, - doc: None, - }], - returns: None, - doc: Some("documented".to_string()), - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let warnings = collect_warnings(&api); - assert!(!warnings - .iter() - .any(|w| matches!(w, ValidationWarning::DeepNesting { .. }))); -} - -#[test] -fn warning_deep_nesting_in_struct_field() { - let deep = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional( - Box::new(TypeRef::List(Box::new(TypeRef::I32))), - ))))); - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![StructDef { - name: "Widget".to_string(), - doc: None, - fields: vec![StructField { - name: "data".to_string(), - ty: deep, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let warnings = collect_warnings(&api); - assert!(warnings.iter().any(|w| matches!( - w, - ValidationWarning::DeepNesting { location, .. } - if location == "mymod::Widget::data" - ))); -} - -#[test] -fn warning_empty_module_doc() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "undocumented".to_string(), - functions: vec![ - Function { - name: "a".to_string(), - params: vec![], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "b".to_string(), - params: vec![], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let warnings = collect_warnings(&api); - assert!(warnings.iter().any(|w| matches!( - w, - ValidationWarning::EmptyModuleDoc { module } if module == "undocumented" - ))); -} - -#[test] -fn warning_partial_docs_no_warning() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "partial".to_string(), - functions: vec![ - Function { - name: "a".to_string(), - params: vec![], - returns: None, - doc: Some("has doc".to_string()), - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "b".to_string(), - params: vec![], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let warnings = collect_warnings(&api); - assert!(!warnings - .iter() - .any(|w| matches!(w, ValidationWarning::EmptyModuleDoc { .. }))); -} - -#[test] -fn warning_no_functions_no_empty_doc_warning() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "empty".to_string(), - functions: vec![], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let warnings = collect_warnings(&api); - assert!(!warnings - .iter() - .any(|w| matches!(w, ValidationWarning::EmptyModuleDoc { .. }))); -} - -#[test] -fn warning_clean_api_no_warnings() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "clean".to_string(), - functions: vec![Function { - name: "add".to_string(), - params: vec![Param { - name: "x".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: Some("Adds numbers".to_string()), - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![simple_enum("Color")], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let warnings = collect_warnings(&api); - assert!(warnings.is_empty()); -} - -#[test] -fn resolve_enum_ref_in_struct_field() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![simple_function("ok_fn")], - interfaces: vec![], - structs: vec![StructDef { - name: "Widget".to_string(), - doc: None, - fields: vec![StructField { - name: "color".to_string(), - ty: TypeRef::Named("Color".to_string()), - doc: None, - }], - }], - enums: vec![simple_enum("Color")], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let api = validate_api(api, None).unwrap(); - assert_eq!( - api.modules[0].structs[0].fields[0].ty, - TypeRef::Enum("Color".to_string()) - ); -} - -#[test] -fn typed_handle_valid_struct_passes() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "get_session".to_string(), - params: vec![Param { - name: "h".to_string(), - ty: TypeRef::TypedHandle("Session".to_string()), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![simple_struct("Session")], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn typed_handle_unknown_struct_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "mymod".to_string(), - functions: vec![Function { - name: "get_session".to_string(), - params: vec![Param { - name: "h".to_string(), - ty: TypeRef::TypedHandle("Nonexistent".to_string()), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::UnknownTypeRef { name } if name == "Nonexistent" - )); -} - -#[test] -fn borrowed_str_param_accepted() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "io".to_string(), - functions: vec![Function { - name: "write".to_string(), - params: vec![Param { - name: "data".to_string(), - ty: TypeRef::BorrowedStr, - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn borrowed_bytes_param_accepted() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "io".to_string(), - functions: vec![Function { - name: "upload".to_string(), - params: vec![Param { - name: "raw".to_string(), - ty: TypeRef::BorrowedBytes, - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn borrowed_str_in_return_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "io".to_string(), - functions: vec![Function { - name: "read".to_string(), - params: vec![], - returns: Some(TypeRef::BorrowedStr), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::BorrowedTypeInInvalidPosition { ty, location } - if ty == "&str" && location.contains("return type") - )); -} - -#[test] -fn borrowed_bytes_in_return_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "io".to_string(), - functions: vec![Function { - name: "read_raw".to_string(), - params: vec![], - returns: Some(TypeRef::BorrowedBytes), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::BorrowedTypeInInvalidPosition { ty, location } - if ty == "&[u8]" && location.contains("return type") - )); -} - -#[test] -fn borrowed_str_in_struct_field_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "data".to_string(), - functions: vec![], - interfaces: vec![], - structs: vec![StructDef { - name: "Msg".to_string(), - fields: vec![StructField { - name: "text".to_string(), - ty: TypeRef::BorrowedStr, - doc: None, - }], - doc: None, - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::BorrowedTypeInInvalidPosition { ty, location } - if ty == "&str" && location.contains("struct") - )); -} - -#[test] -fn borrowed_bytes_in_struct_field_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "data".to_string(), - functions: vec![], - interfaces: vec![], - structs: vec![StructDef { - name: "Blob".to_string(), - fields: vec![StructField { - name: "content".to_string(), - ty: TypeRef::BorrowedBytes, - doc: None, - }], - doc: None, - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::BorrowedTypeInInvalidPosition { ty, location } - if ty == "&[u8]" && location.contains("struct") - )); -} - -#[test] -fn borrowed_str_nested_in_optional_return_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "io".to_string(), - functions: vec![Function { - name: "maybe_read".to_string(), - params: vec![], - returns: Some(TypeRef::Optional(Box::new(TypeRef::BorrowedStr))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; +fn unsupported_schema_version_short_circuits() { + let api = parse_api_str( + "version: '0.1.0'\nmodules: [{ name: a, functions: [] }, { name: a, functions: [] }]", + "yaml", + ) + .unwrap(); + let err = validate_api(api, None).unwrap_err(); + assert_eq!(err.diagnostics.len(), 1); assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::BorrowedTypeInInvalidPosition { ty, .. } - if ty == "&str" + &err.first().error, + ValidationError::UnsupportedSchemaVersion { version, .. } if version == "0.1.0" )); } #[test] -fn cross_module_struct_ref_passes() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![ - Module { - name: "orders".to_string(), - functions: vec![Function { - name: "place_order".to_string(), - params: vec![Param { - name: "item".to_string(), - ty: TypeRef::Named("Product".to_string()), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }, - Module { - name: "catalog".to_string(), - functions: vec![simple_function("list_products")], - interfaces: vec![], - structs: vec![simple_struct("Product")], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }, - ], - generators: None, - package: None, - }; - let api = validate_api(api, None).unwrap(); - assert_eq!( - api.modules[0].functions[0].params[0].ty, - TypeRef::Record("catalog.Product".to_string()) - ); -} - -#[test] -fn cross_module_enum_ref_passes() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![ - Module { - name: "orders".to_string(), - functions: vec![Function { - name: "get_status".to_string(), - params: vec![], - returns: Some(TypeRef::Named("Status".to_string())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }, - Module { - name: "shared".to_string(), - functions: vec![simple_function("noop")], - interfaces: vec![], - structs: vec![], - enums: vec![simple_enum("Status")], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }, - ], - generators: None, - package: None, - }; - let api = validate_api(api, None).unwrap(); - assert_eq!( - api.modules[0].functions[0].returns, - Some(TypeRef::Enum("shared.Status".to_string())) +fn all_violations_are_reported_together() { + let got = errors( + r#" +modules: + - name: m + functions: + - { name: f, params: [], return: Nope } + - { name: f, params: [], throws: true } + structs: [{ name: S, fields: [] }] +"#, ); -} - -#[test] -fn cross_module_unknown_still_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![ - Module { - name: "orders".to_string(), - functions: vec![Function { - name: "do_stuff".to_string(), - params: vec![Param { - name: "x".to_string(), - ty: TypeRef::Named("Nonexistent".to_string()), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }, - Module { - name: "catalog".to_string(), - functions: vec![simple_function("list_products")], - interfaces: vec![], - structs: vec![simple_struct("Product")], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }, - ], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::UnknownTypeRef { name } if name == "Nonexistent" - )); -} - -#[test] -fn find_type_in_api_finds_struct() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "catalog".to_string(), - functions: vec![], - interfaces: vec![], - structs: vec![simple_struct("Product")], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let result = find_type_in_api(&api, "Product"); - assert_eq!(result, Some(("catalog".to_string(), false))); -} - -#[test] -fn find_type_in_api_finds_enum() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "shared".to_string(), - functions: vec![], - interfaces: vec![], - structs: vec![], - enums: vec![simple_enum("Status")], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let result = find_type_in_api(&api, "Status"); - assert_eq!(result, Some(("shared".to_string(), true))); -} - -#[test] -fn find_type_in_api_returns_none_for_unknown() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![simple_module("mymod")], - generators: None, - package: None, - }; - assert_eq!(find_type_in_api(&api, "Nonexistent"), None); -} - -#[test] -fn validate_nested_module_passes() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "parent".to_string(), - functions: vec![simple_function("top_fn")], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![Module { - name: "child".to_string(), - functions: vec![simple_function("inner_fn")], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -fn function_returning(name: &str, ret: TypeRef) -> Function { - Function { - name: name.to_string(), - params: vec![], - returns: Some(ret), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, + for expected in [ + "DuplicateFunctionName", + "UnknownTypeRef", + "ThrowsWithoutErrorDomain", + "EmptyStruct", + ] { + assert!( + got.iter().any(|v| v == expected), + "{expected} missing from {got:?}" + ); } } #[test] -fn find_type_in_api_finds_nested_type() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "graphics".to_string(), - functions: vec![], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![Module { - name: "shapes".to_string(), - functions: vec![], - interfaces: vec![], - structs: vec![simple_struct("Circle")], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - }], - generators: None, - package: None, - }; - // Nested types report their full dot-joined owner path. - assert_eq!( - find_type_in_api(&api, "Circle"), - Some(("graphics.shapes".to_string(), false)) - ); -} - -#[test] -fn resolve_qualifies_reference_to_nested_module_type() { - // A top-level module references a struct defined in one of its own - // nested submodules by bare name; resolution must qualify it to the - // full dotted path so codegen can mangle the right C symbol. - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "app".to_string(), - functions: vec![function_returning( - "make", - TypeRef::Named("Widget".to_string()), - )], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![Module { - name: "ui".to_string(), - functions: vec![], - interfaces: vec![], - structs: vec![simple_struct("Widget")], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - }], - generators: None, - package: None, - }; - let api = validate_api(api, None).unwrap(); +fn diagnostics_render_every_message_and_related() { + let err = check("modules: [{ name: a, functions: [] }, { name: a, functions: [] }, { name: a, functions: [] }]") + .unwrap_err(); + assert_eq!(err.diagnostics.len(), 2); + let text = err.to_string(); + assert_eq!(text.lines().count(), 2); + assert!(text.contains("duplicate module name: a")); + assert_eq!(err.related().map(Iterator::count), Some(1)); + assert!(err.help().is_some()); +} + +#[test] +fn source_spans_underline_the_offending_identifier() { + let src = "version: \"0.8.0\"\nmodules:\n - name: \"dup\"\n - name: \"dup\"\n"; + let api = parse_api_str(src, "yaml").unwrap(); + let err = validate_api(api, Some(("api.yml", src))).unwrap_err(); + let d = err.first(); + let span = d.span.expect("span located"); + assert_eq!(&src[span.offset()..span.offset() + span.len()], "\"dup\""); + assert!(d.labels().is_some()); + assert!(d.source_code().is_some()); +} + +#[test] +fn resolved_api_qualifies_cross_module_types() { + let api = check( + r#" +modules: + - name: shared + enums: [{ name: Status, variants: [{ name: Ok, value: 0 }] }] + structs: [{ name: Point, fields: [{ name: x, type: f64 }] }] + interfaces: [{ name: Store, methods: [{ name: get, params: [] }] }] + - name: orders + functions: + - name: f + params: [{ name: s, type: Store }, { name: st, type: Status }] + return: "[Point]" +"#, + ) + .unwrap(); + use crate::model::Ty; + use weaveffi_ir::ir::TypeRef; + let f = &api.modules[1].functions[0]; assert_eq!( - api.modules[0].functions[0].returns, - Some(TypeRef::Record("app.ui.Widget".to_string())) + api.resolve(&f.params[0].ty, "orders"), + Ty::Interface("shared.Store".into()) ); -} - -#[test] -fn resolve_qualifies_nested_module_reference_to_parent_type() { - // A nested module references a type owned by an ancestor module. Before - // the nested-aware resolver, refs inside submodules were never - // qualified at all; now they resolve to the owner's dotted path. - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "lib".to_string(), - functions: vec![], - interfaces: vec![], - structs: vec![simple_struct("Token")], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![Module { - name: "inner".to_string(), - functions: vec![function_returning( - "fetch", - TypeRef::Named("Token".to_string()), - )], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - }], - generators: None, - package: None, - }; - let api = validate_api(api, None).unwrap(); assert_eq!( - api.modules[0].modules[0].functions[0].returns, - Some(TypeRef::Record("lib.Token".to_string())) + api.resolve(&f.params[1].ty, "orders"), + Ty::Enum("shared.Status".into()) ); -} - -#[test] -fn resolve_qualifies_nested_module_typed_handle_to_parent_type() { - // Regression: a `handle` inside a submodule whose target struct is - // owned by an ancestor module must be qualified to the owner's path. - // Previously the resolver had no `TypedHandle` arm, so it stayed bare - // and every consumer (C ABI lowering + language wrappers) mis-attributed - // it to the *referrer's* prefix (e.g. `weaveffi_kv_stats_Store` instead - // of `weaveffi_kv_Store`), producing an undeclared type. - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "kv".to_string(), - functions: vec![], - interfaces: vec![], - structs: vec![simple_struct("Store")], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![Module { - name: "stats".to_string(), - functions: vec![function_returning( - "get_store", - TypeRef::TypedHandle("Store".to_string()), - )], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - }], - generators: None, - package: None, - }; - let api = validate_api(api, None).unwrap(); assert_eq!( - api.modules[0].modules[0].functions[0].returns, - Some(TypeRef::TypedHandle("kv.Store".to_string())) + api.resolve(f.returns.as_ref().unwrap(), "orders"), + Ty::List(Box::new(Ty::Record("shared.Point".into()))) ); -} - -#[test] -fn resolve_keeps_same_module_typed_handle_unqualified() { - // A `handle` whose target is defined in the *same* module stays bare - // so the lowering keeps using the current module's prefix. - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "sessions".to_string(), - functions: vec![function_returning( - "open", - TypeRef::TypedHandle("Session".to_string()), - )], - interfaces: vec![], - structs: vec![simple_struct("Session")], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let api = validate_api(api, None).unwrap(); assert_eq!( - api.modules[0].functions[0].returns, - Some(TypeRef::TypedHandle("Session".to_string())) + api.resolve(&TypeRef::Named("Point".into()), "shared"), + Ty::Record("Point".into()) ); } #[test] -fn resolve_converts_nested_enum_reference_to_enum_variant() { - // An unqualified reference to an enum that lives in another module must - // be rewritten as `TypeRef::Enum` (not `Struct`) with the dotted path, - // using the global index's `is_enum` flag. - let api = Api { - version: "0.7.0".to_string(), - modules: vec![ - Module { - name: "consumer".to_string(), - functions: vec![function_returning( - "status", - TypeRef::Named("Phase".to_string()), - )], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }, - Module { - name: "shared".to_string(), - functions: vec![], - interfaces: vec![], - structs: vec![], - enums: vec![simple_enum("Phase")], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }, - ], - generators: None, - package: None, - }; - let api = validate_api(api, None).unwrap(); - assert_eq!( - api.modules[0].functions[0].returns, - Some(TypeRef::Enum("shared.Phase".to_string())) - ); -} - -#[test] -fn duplicate_callback_names_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "events".to_string(), - functions: vec![], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![ - CallbackDef { - name: "on_data".to_string(), - params: vec![], - doc: None, - }, - CallbackDef { - name: "on_data".to_string(), - params: vec![], - doc: None, - }, - ], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::DuplicateCallbackName { module, name } - if module == "events" && name == "on_data" - )); -} - -#[test] -fn listener_referencing_undefined_callback_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "events".to_string(), - functions: vec![], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![ListenerDef { - name: "watcher".to_string(), - event_callback: "nonexistent".to_string(), - doc: None, - }], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::ListenerCallbackNotFound { module, listener, callback } - if module == "events" && listener == "watcher" && callback == "nonexistent" - )); -} - -#[test] -fn listener_referencing_defined_callback_passes() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "events".to_string(), - functions: vec![], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![CallbackDef { - name: "on_data".to_string(), - params: vec![Param { - name: "payload".to_string(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - doc: None, - }], - listeners: vec![ListenerDef { - name: "data_stream".to_string(), - event_callback: "on_data".to_string(), - doc: Some("Subscribe to data".to_string()), - }], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn duplicate_listener_names_rejected() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "events".to_string(), - functions: vec![], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![CallbackDef { - name: "on_data".to_string(), - params: vec![], - doc: None, - }], - listeners: vec![ - ListenerDef { - name: "watcher".to_string(), - event_callback: "on_data".to_string(), - doc: None, - }, - ListenerDef { - name: "watcher".to_string(), - event_callback: "on_data".to_string(), - doc: None, - }, - ], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::DuplicateListenerName { module, name } - if module == "events" && name == "watcher" - )); -} - -#[test] -fn iterator_valid_as_return_type() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "data".to_string(), - functions: vec![Function { - name: "list_items".to_string(), - params: vec![], - returns: Some(TypeRef::Iterator(Box::new(TypeRef::I32))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(validate_api(api, None).is_ok()); -} - -#[test] -fn iterator_rejected_as_param() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "data".to_string(), - functions: vec![Function { - name: "consume".to_string(), - params: vec![Param { - name: "items".to_string(), - ty: TypeRef::Iterator(Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::IteratorInInvalidPosition { .. } - )); -} - -#[test] -fn iterator_rejected_in_struct_field() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "data".to_string(), - functions: vec![], - interfaces: vec![], - structs: vec![StructDef { - name: "Container".to_string(), - doc: None, - fields: vec![StructField { - name: "items".to_string(), - ty: TypeRef::Iterator(Box::new(TypeRef::I32)), - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - assert!(matches!( - &validate_api(api, None).unwrap_err().first().error, - ValidationError::IteratorInInvalidPosition { .. } - )); -} - -#[test] -fn mutable_buffered_param_is_error() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "m".into(), - functions: vec![Function { - name: "f".into(), - params: vec![Param { - name: "xs".into(), - ty: TypeRef::List(Box::new(TypeRef::I32)), - mutable: true, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let err = validate_api(api, None).unwrap_err(); - assert!( - matches!( - err.first().error, - ValidationError::MutableParamUnsupported { .. } - ), - "expected MutableParamUnsupported, got: {err}" - ); -} - -#[test] -fn warning_mutable_on_value_type() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "math".to_string(), - functions: vec![Function { - name: "add".to_string(), - params: vec![Param { - name: "x".to_string(), - ty: TypeRef::I32, - mutable: true, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: Some("add".to_string()), - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let warnings = collect_warnings(&api); - assert!(warnings.iter().any(|w| matches!( - w, - ValidationWarning::MutableOnValueType { - param, - .. - } if param == "x" - ))); -} - -#[test] -fn no_warning_mutable_on_pointer_type() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "io".to_string(), - functions: vec![Function { - name: "fill".to_string(), - params: vec![ - Param { - name: "buf".to_string(), - ty: TypeRef::Bytes, - mutable: true, - doc: None, - }, - Param { - name: "msg".to_string(), - ty: TypeRef::StringUtf8, - mutable: true, - doc: None, - }, - Param { - name: "obj".to_string(), - ty: TypeRef::Named("Thing".into()), - mutable: true, - doc: None, - }, - ], - returns: None, - doc: Some("fill".to_string()), - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let warnings = collect_warnings(&api); - assert!( - !warnings - .iter() - .any(|w| matches!(w, ValidationWarning::MutableOnValueType { .. })), - "pointer types should not trigger mutable warning" - ); -} - -#[test] -fn no_warning_mutable_false_on_value_type() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "math".to_string(), - functions: vec![Function { - name: "add".to_string(), - params: vec![Param { - name: "x".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: Some("add".to_string()), - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let warnings = collect_warnings(&api); - assert!( - !warnings - .iter() - .any(|w| matches!(w, ValidationWarning::MutableOnValueType { .. })), - "mutable=false should not trigger warning" - ); -} - -#[test] -fn warning_mutable_on_enum_type() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "paint".to_string(), - functions: vec![Function { - name: "set_color".to_string(), - params: vec![Param { - name: "color".to_string(), - ty: TypeRef::Enum("Color".into()), - mutable: true, - doc: None, - }], - returns: None, - doc: Some("set".to_string()), - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; +fn warnings_are_advisory() { + let api = check( + r#" +modules: + - name: m + enums: + - name: Big + variants: + - { name: A, value: 0 } + functions: + - { name: deep, params: [{ name: x, type: "[[[[i32]]]]" }] } + - { name: fire, params: [], async: true } + - { name: old, params: [], deprecated: "use new" } + modules: + - name: documented + doc: Has a module doc. + functions: [{ name: f, params: [] }] +"#, + ) + .unwrap(); let warnings = collect_warnings(&api); - assert!(warnings.iter().any(|w| matches!( - w, - ValidationWarning::MutableOnValueType { param, .. } if param == "color" - ))); -} - -#[test] -fn warning_deprecated_function() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "math".to_string(), - functions: vec![Function { - name: "add_old".to_string(), - params: vec![], - returns: Some(TypeRef::I32), - doc: Some("old add".to_string()), - throws: false, - r#async: false, - cancellable: false, - deprecated: Some("Use add_v2 instead".to_string()), - since: Some("0.1.0".to_string()), - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let warnings = collect_warnings(&api); - assert!(warnings.iter().any(|w| matches!( - w, - ValidationWarning::DeprecatedFunction { function, message, .. } - if function == "add_old" && message == "Use add_v2 instead" - ))); -} - -#[test] -fn no_warning_for_non_deprecated_function() { - let api = Api { - version: "0.7.0".to_string(), - modules: vec![Module { - name: "math".to_string(), - functions: vec![Function { - name: "add".to_string(), - params: vec![], - returns: Some(TypeRef::I32), - doc: Some("add things".to_string()), - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }; - let warnings = collect_warnings(&api); - assert!(!warnings + let kinds: Vec = warnings .iter() - .any(|w| matches!(w, ValidationWarning::DeprecatedFunction { .. }))); + .map(|w| format!("{w:?}").split(' ').next().unwrap().to_string()) + .collect(); + assert_eq!( + kinds, + [ + "DeepNesting", + "AsyncVoidFunction", + "DeprecatedFunction", + "EmptyModuleDoc" + ] + ); + assert!(warnings[0].to_string().contains("depth 4")); + assert!(warnings[2].to_string().contains("use new")); } diff --git a/crates/weaveffi-core/src/validate/warnings.rs b/crates/weaveffi-core/src/validate/warnings.rs index 5b3b03b6..3a8072d0 100644 --- a/crates/weaveffi-core/src/validate/warnings.rs +++ b/crates/weaveffi-core/src/validate/warnings.rs @@ -3,13 +3,19 @@ //! These are distinct from the hard validation errors in the parent //! [`crate::validate`] module: errors *reject* an IDL, whereas warnings //! merely flag stylistic or ergonomic concerns (deep nesting, undocumented -//! modules, no-op `mutable` flags, …) that the caller can surface and the -//! user can choose to ignore. +//! modules, async functions with no result) that the caller can surface and +//! the user can choose to ignore. -use weaveffi_ir::ir::{Api, TypeRef}; +use weaveffi_ir::ir::{Api, Module, TypeRef}; + +/// Nesting deeper than this (`[[[[i32]]]]`) is flagged. +const MAX_NESTING: usize = 3; + +/// Enums with more variants than this are flagged. +const MAX_ENUM_VARIANTS: usize = 100; /// A non-fatal advisory emitted by [`collect_warnings`]. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum ValidationWarning { /// An enum has an unusually large number of variants (more than 100). LargeEnumVariantCount { @@ -25,9 +31,10 @@ pub enum ValidationWarning { /// Measured nesting depth. depth: usize, }, - /// A module has functions but none of them carry a doc comment. + /// A module declares functions but carries no doc comment on itself or + /// on any of them. EmptyModuleDoc { - /// Module with no documented functions. + /// Module with no documentation. module: String, }, /// An async function declares no return type, which is unusual. @@ -37,15 +44,6 @@ pub enum ValidationWarning { /// Async function with no return type. function: String, }, - /// A `mutable` flag sits on a value-type parameter, where it has no effect. - MutableOnValueType { - /// Module that contains the function. - module: String, - /// Function that declares the parameter. - function: String, - /// Parameter that carries the no-op `mutable` flag. - param: String, - }, /// A function is marked deprecated; the message is surfaced to consumers. DeprecatedFunction { /// Module that contains the function. @@ -61,140 +59,102 @@ impl std::fmt::Display for ValidationWarning { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::LargeEnumVariantCount { enum_name, count } => { - write!(f, "enum '{enum_name}' has {count} variants (>100)") - } - Self::DeepNesting { location, depth } => { write!( f, - "deep type nesting at {location} (depth {depth}, max recommended 3)" + "enum '{enum_name}' has {count} variants (>{MAX_ENUM_VARIANTS})" ) } + Self::DeepNesting { location, depth } => write!( + f, + "deep type nesting at {location} (depth {depth}, max recommended {MAX_NESTING})" + ), Self::EmptyModuleDoc { module } => { - write!(f, "module '{module}' has no doc comments on any function") - } - Self::AsyncVoidFunction { module, function } => { write!( f, - "async function {module}::{function} has no return type; async void is unusual" - ) - } - Self::MutableOnValueType { - module, - function, - param, - } => { - write!( - f, - "'mutable' on value-type parameter {module}::{function}::{param} has no effect; only meaningful for pointer/reference types (struct, string, bytes)" + "module '{module}' has no doc comment on itself or any function" ) } + Self::AsyncVoidFunction { module, function } => write!( + f, + "async function {module}::{function} has no return type; async void is unusual" + ), Self::DeprecatedFunction { module, function, message, - } => { - write!(f, "function {module}::{function} is deprecated: {message}") - } + } => write!(f, "function {module}::{function} is deprecated: {message}"), } } } -/// Walk every module and collect all advisory warnings for `api`. +/// Walk every module (including nested submodules) and collect all advisory +/// warnings for `api`. /// /// Assumes `api` has already passed hard validation; it does not re-check /// structural invariants. pub fn collect_warnings(api: &Api) -> Vec { let mut warnings = Vec::new(); for module in &api.modules { - for e in &module.enums { - if e.variants.len() > 100 { - warnings.push(ValidationWarning::LargeEnumVariantCount { - enum_name: e.name.clone(), - count: e.variants.len(), - }); - } - } + collect_module(module, &mut warnings); + } + warnings +} - for f in &module.functions { - for p in &f.params { - let depth = nesting_depth(&p.ty); - if depth > 3 { - warnings.push(ValidationWarning::DeepNesting { - location: format!("{}::{}::{}", module.name, f.name, p.name), - depth, - }); - } - } - if let Some(ret) = &f.returns { - let depth = nesting_depth(ret); - if depth > 3 { - warnings.push(ValidationWarning::DeepNesting { - location: format!("{}::{}::return", module.name, f.name), - depth, - }); - } - } +fn collect_module(module: &Module, warnings: &mut Vec) { + let name = &module.name; + for e in &module.enums { + if e.variants.len() > MAX_ENUM_VARIANTS { + warnings.push(ValidationWarning::LargeEnumVariantCount { + enum_name: e.name.clone(), + count: e.variants.len(), + }); } - for s in &module.structs { - for field in &s.fields { - let depth = nesting_depth(&field.ty); - if depth > 3 { - warnings.push(ValidationWarning::DeepNesting { - location: format!("{}::{}::{}", module.name, s.name, field.name), - depth, - }); - } - } + } + let mut deep = |ty: &TypeRef, location: String| { + let depth = nesting_depth(ty); + if depth > MAX_NESTING { + warnings.push(ValidationWarning::DeepNesting { location, depth }); } - - for f in &module.functions { - if f.r#async && f.returns.is_none() { - warnings.push(ValidationWarning::AsyncVoidFunction { - module: module.name.clone(), - function: f.name.clone(), - }); - } - for p in &f.params { - if p.mutable && is_value_type(&p.ty) { - warnings.push(ValidationWarning::MutableOnValueType { - module: module.name.clone(), - function: f.name.clone(), - param: p.name.clone(), - }); - } - } + }; + for s in &module.structs { + for field in &s.fields { + deep(&field.ty, format!("{name}::{}::{}", s.name, field.name)); } - - for f in &module.functions { - if let Some(msg) = &f.deprecated { - warnings.push(ValidationWarning::DeprecatedFunction { - module: module.name.clone(), - function: f.name.clone(), - message: msg.clone(), - }); - } + } + for f in &module.functions { + for p in &f.params { + deep(&p.ty, format!("{name}::{}::{}", f.name, p.name)); } - - if !module.functions.is_empty() && module.functions.iter().all(|f| f.doc.is_none()) { - warnings.push(ValidationWarning::EmptyModuleDoc { - module: module.name.clone(), + if let Some(ret) = &f.returns { + deep(ret, format!("{name}::{}::return", f.name)); + } + } + for f in &module.functions { + if f.r#async && f.returns.is_none() { + warnings.push(ValidationWarning::AsyncVoidFunction { + module: name.clone(), + function: f.name.clone(), + }); + } + if let Some(msg) = &f.deprecated { + warnings.push(ValidationWarning::DeprecatedFunction { + module: name.clone(), + function: f.name.clone(), + message: msg.clone(), }); } } - warnings -} - -fn is_value_type(ty: &TypeRef) -> bool { - matches!( - ty, - TypeRef::I32 - | TypeRef::U32 - | TypeRef::I64 - | TypeRef::F64 - | TypeRef::Bool - | TypeRef::Enum(_) - | TypeRef::Handle - ) + if module.doc.is_none() + && !module.functions.is_empty() + && module.functions.iter().all(|f| f.doc.is_none()) + { + warnings.push(ValidationWarning::EmptyModuleDoc { + module: name.clone(), + }); + } + for child in &module.modules { + collect_module(child, warnings); + } } fn nesting_depth(ty: &TypeRef) -> usize { @@ -202,7 +162,7 @@ fn nesting_depth(ty: &TypeRef) -> usize { TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => { 1 + nesting_depth(inner) } - TypeRef::Map(k, v) => nesting_depth(k).max(nesting_depth(v)), + TypeRef::Map(k, v) => 1 + nesting_depth(k).max(nesting_depth(v)), _ => 0, } } diff --git a/crates/weaveffi-core/src/wire.rs b/crates/weaveffi-core/src/wire.rs deleted file mode 100644 index c18a0ce2..00000000 --- a/crates/weaveffi-core/src/wire.rs +++ /dev/null @@ -1,224 +0,0 @@ -//! The canonical **wire classification**: how each [`TypeRef`] is encoded -//! inside a value buffer, stated once for every backend. -//! -//! The value-buffer format itself (byte order, lengths, tags) is specified by -//! `weaveffi-abi`'s `buffer` module and summarized in -//! [`docs/src/reference/value-buffers.md`](https://weaveffi.com/reference/value-buffers.html). -//! What each backend needs on top of that spec is a *dispatch decision*: given -//! an IR type, which encode/decode primitive applies? Before this module -//! existed, all eleven generators answered that with their own `TypeRef` -//! match, each carrying its own copy of the non-obvious folds (handles encode -//! as `u64` tokens, borrowed views encode like their owned forms, records and -//! rich enums share one user-codec shape) and its own `unreachable!` arms. -//! They could drift; now they cannot. -//! -//! [`classify`] folds a `TypeRef` into a [`WireType`], the closed set of wire -//! shapes a buffer can contain. A backend's codec emitter matches on -//! `WireType` and never on `TypeRef`, so every "which primitive?" decision is -//! made here, once. - -use weaveffi_ir::ir::TypeRef; - -/// The closed set of shapes a value inside a value buffer can take. -/// -/// This is the dispatch alphabet for every backend's buffer codec: one -/// variant per encode/decode primitive of the wire format. The borrowed -/// lifetimes point back into the classified [`TypeRef`], so classification -/// allocates nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum WireType<'a> { - /// One byte, `0` or `1`. - Bool, - /// One signed byte. - I8, - /// Two bytes, little-endian, signed. - I16, - /// Four bytes, little-endian, signed. Also the encoding of C-style enum - /// discriminants and rich-enum tags, but those classify as - /// [`Enum`](Self::Enum) and [`User`](Self::User) so backends can emit - /// typed wrappers. - I32, - /// Eight bytes, little-endian, signed. - I64, - /// One unsigned byte. - U8, - /// Two bytes, little-endian, unsigned. - U16, - /// Four bytes, little-endian, unsigned. - U32, - /// Eight bytes, little-endian, unsigned. - U64, - /// Four bytes, IEEE 754 bits. - F32, - /// Eight bytes, IEEE 754 bits. - F64, - /// An opaque handle token: eight bytes, little-endian, unsigned. Covers - /// both the untyped `handle` and `handle`; the referent name plays no - /// part in the encoding. - Handle, - /// A `u32` byte length followed by UTF-8 bytes, no NUL terminator. - /// Covers `string` and `&str` (a borrowed view encodes exactly like its - /// owned form; borrowing is a call-boundary concern, not a wire one). - String, - /// A `u32` length followed by raw bytes. Covers `bytes` and `&[u8]`. - Bytes, - /// A C-style enum: an `i32` discriminant. Carries the (possibly - /// dot-qualified) enum name so backends can wrap the integer in their - /// typed enum. - Enum(&'a str), - /// A record or rich enum: the named type's own codec applies (fields in - /// declaration order for a record; an `i32` tag plus the active - /// variant's fields for a rich enum). Backends emit one codec function - /// per user type and delegate here by name (possibly dot-qualified). - User(&'a str), - /// A one-byte presence flag (`0` absent, `1` present) followed by the - /// inner value when present. - Optional(&'a TypeRef), - /// A `u32` element count followed by each element. - List(&'a TypeRef), - /// A `u32` entry count followed by alternating key and value. - Map(&'a TypeRef, &'a TypeRef), -} - -/// Classify `ty` into its wire shape. -/// -/// This is total over every type validation admits inside a buffered -/// position. Interfaces and iterators never appear inside value buffers -/// (validation rejects them there), and no unresolved [`TypeRef::Named`] -/// survives a successful validate-and-resolve, so those inputs are bugs in -/// the caller's pipeline, not user errors. -/// -/// # Panics -/// -/// Panics when `ty` is an interface, an iterator, or an unresolved named -/// reference, none of which can legally appear inside a value buffer. -pub fn classify(ty: &TypeRef) -> WireType<'_> { - match ty { - TypeRef::Bool => WireType::Bool, - TypeRef::I8 => WireType::I8, - TypeRef::I16 => WireType::I16, - TypeRef::I32 => WireType::I32, - TypeRef::I64 => WireType::I64, - TypeRef::U8 => WireType::U8, - TypeRef::U16 => WireType::U16, - TypeRef::U32 => WireType::U32, - TypeRef::U64 => WireType::U64, - TypeRef::F32 => WireType::F32, - TypeRef::F64 => WireType::F64, - TypeRef::Handle | TypeRef::TypedHandle(_) => WireType::Handle, - TypeRef::StringUtf8 | TypeRef::BorrowedStr => WireType::String, - TypeRef::Bytes | TypeRef::BorrowedBytes => WireType::Bytes, - TypeRef::Enum(name) => WireType::Enum(name), - TypeRef::Record(name) | TypeRef::RichEnum(name) => WireType::User(name), - TypeRef::Optional(inner) => WireType::Optional(inner), - TypeRef::List(inner) => WireType::List(inner), - TypeRef::Map(k, v) => WireType::Map(k, v), - TypeRef::Interface(_) | TypeRef::Iterator(_) => { - panic!("object references cannot appear inside value buffers: {ty:?}") - } - TypeRef::Named(n) => { - panic!("unresolved type reference '{n}' reached wire classification") - } - } -} - -/// `true` when a value of `ty` needs a user-defined codec function somewhere -/// in its encoding: it is (or transitively contains) a record or rich enum. -/// -/// Backends use this to decide whether a helper codec must be emitted before -/// an inline expression can decode the type. -pub fn contains_user_type(ty: &TypeRef) -> bool { - match ty { - TypeRef::Record(_) | TypeRef::RichEnum(_) => true, - TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => { - contains_user_type(inner) - } - TypeRef::Map(k, v) => contains_user_type(k) || contains_user_type(v), - _ => false, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn scalars_classify_directly() { - assert_eq!(classify(&TypeRef::Bool), WireType::Bool); - assert_eq!(classify(&TypeRef::I32), WireType::I32); - assert_eq!(classify(&TypeRef::U64), WireType::U64); - assert_eq!(classify(&TypeRef::F64), WireType::F64); - } - - #[test] - fn handles_fold_to_one_token_shape() { - assert_eq!(classify(&TypeRef::Handle), WireType::Handle); - assert_eq!( - classify(&TypeRef::TypedHandle("Session".into())), - WireType::Handle - ); - } - - #[test] - fn borrowed_views_encode_like_owned() { - assert_eq!(classify(&TypeRef::StringUtf8), WireType::String); - assert_eq!(classify(&TypeRef::BorrowedStr), WireType::String); - assert_eq!(classify(&TypeRef::Bytes), WireType::Bytes); - assert_eq!(classify(&TypeRef::BorrowedBytes), WireType::Bytes); - } - - #[test] - fn records_and_rich_enums_share_the_user_shape() { - assert_eq!( - classify(&TypeRef::Record("Contact".into())), - WireType::User("Contact") - ); - assert_eq!( - classify(&TypeRef::RichEnum("Shape".into())), - WireType::User("Shape") - ); - assert_eq!( - classify(&TypeRef::Enum("Color".into())), - WireType::Enum("Color") - ); - } - - #[test] - fn composites_expose_their_inner_types() { - let list = TypeRef::List(Box::new(TypeRef::I32)); - assert_eq!(classify(&list), WireType::List(&TypeRef::I32)); - let opt = TypeRef::Optional(Box::new(TypeRef::StringUtf8)); - assert_eq!(classify(&opt), WireType::Optional(&TypeRef::StringUtf8)); - let map = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)); - assert_eq!( - classify(&map), - WireType::Map(&TypeRef::StringUtf8, &TypeRef::I32) - ); - } - - #[test] - #[should_panic(expected = "object references")] - fn interfaces_panic() { - classify(&TypeRef::Interface("Store".into())); - } - - #[test] - #[should_panic(expected = "unresolved")] - fn named_panics() { - classify(&TypeRef::Named("Contact".into())); - } - - #[test] - fn contains_user_type_recurses() { - assert!(contains_user_type(&TypeRef::Record("C".into()))); - assert!(contains_user_type(&TypeRef::List(Box::new( - TypeRef::RichEnum("S".into()) - )))); - assert!(contains_user_type(&TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::Optional(Box::new(TypeRef::Record("C".into())))) - ))); - assert!(!contains_user_type(&TypeRef::List(Box::new(TypeRef::I32)))); - assert!(!contains_user_type(&TypeRef::Enum("Color".into()))); - } -} diff --git a/crates/weaveffi-core/tests/codegen_hooks.rs b/crates/weaveffi-core/tests/codegen_hooks.rs index ddf0c133..49fba1dc 100644 --- a/crates/weaveffi-core/tests/codegen_hooks.rs +++ b/crates/weaveffi-core/tests/codegen_hooks.rs @@ -8,88 +8,63 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; -use anyhow::Result; use camino::Utf8Path; -use weaveffi_core::codegen::{ConfiguredGenerator, Generator, Orchestrator, OrchestratorHooks}; +use weaveffi_core::backend::{LanguageBackend, OutputFile}; +use weaveffi_core::capabilities::TargetCapabilities; +use weaveffi_core::codegen::{ConfiguredBackend, Orchestrator, OrchestratorHooks}; +use weaveffi_core::model::BindingModel; use weaveffi_core::resolved::ResolvedApi; -use weaveffi_ir::ir::{Api, Function, Module, Param, TypeRef}; +use weaveffi_ir::ir::{Api, Module, CURRENT_SCHEMA_VERSION}; const HOOK_HELPER: &str = env!("CARGO_BIN_EXE_hook_helper"); -fn quote_arg(arg: &str) -> String { - if arg.contains(' ') || arg.contains('"') { - format!("\"{}\"", arg.replace('"', "\\\"")) - } else { - arg.to_string() - } -} - fn helper_cmd(arg: &str) -> String { - format!("{} {}", quote_arg(HOOK_HELPER), arg) + let exe = if HOOK_HELPER.contains(' ') || HOOK_HELPER.contains('"') { + format!("\"{}\"", HOOK_HELPER.replace('"', "\\\"")) + } else { + HOOK_HELPER.to_string() + }; + format!("{exe} {arg}") } -#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Default, Clone, serde::Serialize)] struct TestConfig; -struct CountingGenerator { - name: &'static str, - calls: Arc, -} +struct Counting(Arc); -impl Generator for CountingGenerator { +impl LanguageBackend for Counting { type Config = TestConfig; fn name(&self) -> &'static str { - self.name + "counting" } - fn capabilities(&self) -> weaveffi_core::capabilities::TargetCapabilities { - weaveffi_core::capabilities::TargetCapabilities::full() + fn capabilities(&self) -> TargetCapabilities { + TargetCapabilities::full() } - fn generate( + fn files( &self, _api: &ResolvedApi, + _model: &BindingModel, out_dir: &Utf8Path, _config: &Self::Config, - ) -> Result<()> { - self.calls.fetch_add(1, Ordering::SeqCst); - let dir = out_dir.join(self.name); - std::fs::create_dir_all(dir.as_std_path())?; - std::fs::write(dir.join("output.txt").as_std_path(), "generated")?; - Ok(()) + ) -> Vec { + self.0.fetch_add(1, Ordering::SeqCst); + vec![OutputFile::new( + out_dir.join("counting/output.txt"), + "generated", + )] } } -fn test_api() -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: "0.5.0".to_string(), +fn api() -> ResolvedApi { + ResolvedApi::assume_valid(Api { + version: CURRENT_SCHEMA_VERSION.into(), modules: vec![Module { - name: "math".to_string(), - functions: vec![Function { - name: "add".to_string(), - params: vec![ - Param { - name: "a".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], + name: "math".into(), + doc: None, + functions: vec![], interfaces: vec![], structs: vec![], enums: vec![], @@ -98,84 +73,41 @@ fn test_api() -> ResolvedApi { errors: None, modules: vec![], }], - generators: None, - package: None, }) } -fn configured( - name: &'static str, - calls: Arc, -) -> ConfiguredGenerator { - ConfiguredGenerator::new(CountingGenerator { name, calls }, TestConfig) -} - -#[test] -fn pre_hook_runs_before_generate() { +/// Run one orchestration with `hooks`, returning `(result, generate_calls)`. +fn run(hooks: OrchestratorHooks) -> (anyhow::Result<()>, usize) { let dir = tempfile::tempdir().unwrap(); let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let api = test_api(); - let hooks = OrchestratorHooks { - pre_generate: Some(helper_cmd("ok")), - ..OrchestratorHooks::default() - }; let calls = Arc::new(AtomicUsize::new(0)); - let gen = configured("counting", Arc::clone(&calls)); - - let orch = Orchestrator::new().with_generator(&gen); - orch.run(&api, out_dir, &hooks, true).unwrap(); - assert_eq!(calls.load(Ordering::SeqCst), 1); + let target = ConfiguredBackend::new(Counting(Arc::clone(&calls)), TestConfig); + let result = Orchestrator::new() + .with_target(&target) + .run(&api(), out_dir, &hooks, true); + (result, calls.load(Ordering::SeqCst)) } #[test] -fn pre_hook_failure_aborts() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let api = test_api(); - let hooks = OrchestratorHooks { - pre_generate: Some(helper_cmd("fail")), - ..OrchestratorHooks::default() - }; - let calls = Arc::new(AtomicUsize::new(0)); - let gen = configured("counting", Arc::clone(&calls)); - - let orch = Orchestrator::new().with_generator(&gen); - let result = orch.run(&api, out_dir, &hooks, true); - assert!(result.is_err()); - assert_eq!(calls.load(Ordering::SeqCst), 0, "generator should not run"); -} - -#[test] -fn post_hook_runs_after_generate() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let api = test_api(); - let hooks = OrchestratorHooks { +fn hooks_run_around_generation_and_failures_propagate() { + let (ok, calls) = run(OrchestratorHooks { + pre_generate: Some(helper_cmd("ok")), post_generate: Some(helper_cmd("ok")), - ..OrchestratorHooks::default() - }; - let calls = Arc::new(AtomicUsize::new(0)); - let gen = configured("counting", Arc::clone(&calls)); + }); + ok.unwrap(); + assert_eq!(calls, 1); - let orch = Orchestrator::new().with_generator(&gen); - orch.run(&api, out_dir, &hooks, true).unwrap(); - assert_eq!(calls.load(Ordering::SeqCst), 1); -} + let (err, calls) = run(OrchestratorHooks { + pre_generate: Some(helper_cmd("fail")), + post_generate: None, + }); + assert!(err.is_err()); + assert_eq!(calls, 0, "a failing pre hook must prevent generation"); -#[test] -fn post_hook_failure_returns_error() { - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - let api = test_api(); - let hooks = OrchestratorHooks { + let (err, calls) = run(OrchestratorHooks { + pre_generate: None, post_generate: Some(helper_cmd("fail")), - ..OrchestratorHooks::default() - }; - let calls = Arc::new(AtomicUsize::new(0)); - let gen = configured("counting", Arc::clone(&calls)); - - let orch = Orchestrator::new().with_generator(&gen); - let result = orch.run(&api, out_dir, &hooks, true); - assert!(result.is_err()); - assert_eq!(calls.load(Ordering::SeqCst), 1, "generator should have run"); + }); + assert!(err.is_err()); + assert_eq!(calls, 1, "the post hook runs after generation"); } 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 24dcc7af..780bdf48 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.7.0", + "version": "0.8.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 index 305a078c..879d7af2 100644 --- a/crates/weaveffi-fuzz/fuzz/seeds/fuzz_parse_toml/minimal.toml +++ b/crates/weaveffi-fuzz/fuzz/seeds/fuzz_parse_toml/minimal.toml @@ -1,4 +1,4 @@ -version = "0.7.0" +version = "0.8.0" [[modules]] name = "m" 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 5082db36..202af628 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.7.0" +version: "0.8.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 a4a83d89..846d3901 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.7.0" +version: "0.8.0" modules: - name: m functions: diff --git a/crates/weaveffi-gen-android/Cargo.toml b/crates/weaveffi-gen-android/Cargo.toml index 4c23f7cf..294d6f3a 100644 --- a/crates/weaveffi-gen-android/Cargo.toml +++ b/crates/weaveffi-gen-android/Cargo.toml @@ -11,13 +11,8 @@ categories = ["development-tools::ffi"] [dependencies] weaveffi-core = { path = "../weaveffi-core", version = "0.21.0" } -weaveffi-ir = { path = "../weaveffi-ir", version = "0.21.0" } camino = { workspace = true } serde = { workspace = true } -[dev-dependencies] -tempfile = { workspace = true } -weaveffi-gen-c = { path = "../weaveffi-gen-c", version = "0.21.0" } - [lints] workspace = true diff --git a/crates/weaveffi-gen-android/src/calls.rs b/crates/weaveffi-gen-android/src/calls.rs index bea4376a..5ffdca18 100644 --- a/crates/weaveffi-gen-android/src/calls.rs +++ b/crates/weaveffi-gen-android/src/calls.rs @@ -3,16 +3,15 @@ use std::fmt::Write as _; -use weaveffi_core::abi; use weaveffi_core::codegen::common::pascal_case; use weaveffi_core::codegen::CodeWriter; use weaveffi_core::errors; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ CallShape, ErrorBinding, FnBinding, ListenerBinding, ModuleBinding, ParamBinding, }; use weaveffi_core::plan::ErrorStrategy; use weaveffi_core::utils::local_type_name; -use weaveffi_ir::ir::TypeRef; use crate::codec::{kt_decode_expr, kt_encode_expr}; use crate::docs::emit_doc; @@ -82,7 +81,7 @@ pub(crate) fn render_listener_api( // Buffered callback arguments arrive as raw `ByteArray` copies: // the public register wrapper decodes them before the user's // lambda runs, so the JNI-facing external takes a wrapper lambda. - let has_buffered = cb.params.iter().any(|p| abi::is_buffered(&p.ty)); + let has_buffered = cb.params.iter().any(|p| p.ty.is_buffered()); if has_buffered { let jni_params: Vec = cb .params @@ -100,7 +99,7 @@ pub(crate) fn render_listener_api( .iter() .enumerate() .map(|(i, p)| { - if abi::is_buffered(&p.ty) { + if p.ty.is_buffered() { kt_decode_expr(&p.ty, &format!("a{i}")) } else { format!("a{i}") @@ -212,14 +211,14 @@ pub(crate) fn render_kotlin_free_fn( /// pass the raw `.handle` (nullable via `?.`). pub(crate) fn kotlin_unwrap_arg(p: &ParamBinding) -> String { let n = kt_param(&p.name); - if abi::is_buffered(&p.ty) { + if p.ty.is_buffered() { return kt_encode_expr(&p.ty, &n); } match &p.ty { - TypeRef::Enum(_) => format!("{n}.value"), - TypeRef::Interface(_) => format!("{n}.handle"), + Ty::Enum(_) => format!("{n}.value"), + Ty::Interface(_) => format!("{n}.handle"), // Only `Interface?` reaches here (every other optional is buffered). - TypeRef::Optional(_) => format!("{n}?.handle"), + Ty::Optional(_) => format!("{n}?.handle"), _ => n, } } @@ -229,19 +228,17 @@ pub(crate) fn kotlin_unwrap_arg(p: &ParamBinding) -> String { /// type: buffered returns decode the `ByteArray`, enums round-trip through /// `fromValue`, interfaces through the class constructor (nullable via /// `?.let`). -pub(crate) fn kotlin_wrap_return(ret: Option<&TypeRef>, expr: &str) -> Option { +pub(crate) fn kotlin_wrap_return(ret: Option<&Ty>, expr: &str) -> Option { let ret = ret?; - if abi::is_buffered(ret) { + if ret.is_buffered() { return Some(kt_decode_expr(ret, expr)); } match ret { - TypeRef::Enum(name) => Some(format!("{}.fromValue({expr})", local_type_name(name))), - TypeRef::Interface(name) => Some(format!("{}({expr})", local_type_name(name))), + Ty::Enum(name) => Some(format!("{}.fromValue({expr})", local_type_name(name))), + Ty::Interface(name) => Some(format!("{}({expr})", local_type_name(name))), // Only `Interface?` reaches here (every other optional is buffered). - TypeRef::Optional(inner) => match inner.as_ref() { - TypeRef::Interface(name) => { - Some(format!("{expr}?.let {{ {}(it) }}", local_type_name(name))) - } + Ty::Optional(inner) => match inner.as_ref() { + Ty::Interface(name) => Some(format!("{expr}?.let {{ {}(it) }}", local_type_name(name))), _ => unreachable!("buffered optionals are handled above"), }, _ => None, diff --git a/crates/weaveffi-gen-android/src/codec.rs b/crates/weaveffi-gen-android/src/codec.rs index 16ce2cff..1efe961d 100644 --- a/crates/weaveffi-gen-android/src/codec.rs +++ b/crates/weaveffi-gen-android/src/codec.rs @@ -1,11 +1,10 @@ //! Value-buffer codec expressions: the Kotlin read and write expression for //! every wire shape, dispatched on the shared [`wire::classify`] fold. -use weaveffi_core::abi; +use weaveffi_core::model::Ty; use weaveffi_core::model::{BindingModel, CallShape, EnumBinding}; +use weaveffi_core::model::{Prim, WireType}; use weaveffi_core::utils::local_type_name; -use weaveffi_core::wire::{self, WireType}; -use weaveffi_ir::ir::TypeRef; use crate::types::kt_escape; @@ -13,19 +12,21 @@ use crate::types::kt_escape; /// `t`) into the value-buffer writer named `w`. Optionals, lists, and maps /// recurse through the writer's lambda helpers; `depth` uniquifies the lambda /// parameter names at each nesting level. -pub(crate) fn kt_write_expr(t: &TypeRef, w: &str, expr: &str, depth: usize) -> String { - match wire::classify(t) { - WireType::Bool => format!("{w}.writeBool({expr})"), - WireType::I8 | WireType::U8 => format!("{w}.writeI8({expr})"), - WireType::I16 | WireType::U16 => format!("{w}.writeI16({expr})"), - WireType::I32 => format!("{w}.writeI32({expr})"), - WireType::U32 => format!("{w}.writeU32({expr})"), - WireType::I64 | WireType::U64 | WireType::Handle => format!("{w}.writeI64({expr})"), - WireType::F32 => format!("{w}.writeF32({expr})"), - WireType::F64 => format!("{w}.writeF64({expr})"), +pub(crate) fn kt_write_expr(t: &Ty, w: &str, expr: &str, depth: usize) -> String { + match t.wire() { + WireType::Prim(Prim::Bool) => format!("{w}.writeBool({expr})"), + WireType::Prim(Prim::I8) | WireType::Prim(Prim::U8) => format!("{w}.writeI8({expr})"), + WireType::Prim(Prim::I16) | WireType::Prim(Prim::U16) => format!("{w}.writeI16({expr})"), + WireType::Prim(Prim::I32) => format!("{w}.writeI32({expr})"), + WireType::Prim(Prim::U32) => format!("{w}.writeU32({expr})"), + WireType::Prim(Prim::I64) | WireType::Prim(Prim::U64) | WireType::Handle(_) => { + format!("{w}.writeI64({expr})") + } + WireType::Prim(Prim::F32) => format!("{w}.writeF32({expr})"), + WireType::Prim(Prim::F64) => format!("{w}.writeF64({expr})"), WireType::Enum(_) => format!("{w}.writeI32({expr}.value)"), - WireType::String => format!("{w}.writeString({expr})"), - WireType::Bytes => format!("{w}.writeBytes({expr})"), + WireType::Prim(Prim::String) => format!("{w}.writeString({expr})"), + WireType::Prim(Prim::Bytes) => format!("{w}.writeBytes({expr})"), WireType::User(name) => format!("pack{}({w}, {expr})", local_type_name(name)), WireType::Optional(inner) => { let v = format!("v{depth}"); @@ -55,19 +56,21 @@ pub(crate) fn kt_write_expr(t: &TypeRef, w: &str, expr: &str, depth: usize) -> S /// The Kotlin expression reading a value of type `t` from the value-buffer /// reader named `r`. The inverse of [`kt_write_expr`]. -pub(crate) fn kt_read_expr(t: &TypeRef, r: &str) -> String { - match wire::classify(t) { - WireType::Bool => format!("{r}.readBool()"), - WireType::I8 | WireType::U8 => format!("{r}.readI8()"), - WireType::I16 | WireType::U16 => format!("{r}.readI16()"), - WireType::I32 => format!("{r}.readI32()"), - WireType::U32 => format!("{r}.readU32()"), - WireType::I64 | WireType::U64 | WireType::Handle => format!("{r}.readI64()"), - WireType::F32 => format!("{r}.readF32()"), - WireType::F64 => format!("{r}.readF64()"), +pub(crate) fn kt_read_expr(t: &Ty, r: &str) -> String { + match t.wire() { + WireType::Prim(Prim::Bool) => format!("{r}.readBool()"), + WireType::Prim(Prim::I8) | WireType::Prim(Prim::U8) => format!("{r}.readI8()"), + WireType::Prim(Prim::I16) | WireType::Prim(Prim::U16) => format!("{r}.readI16()"), + WireType::Prim(Prim::I32) => format!("{r}.readI32()"), + WireType::Prim(Prim::U32) => format!("{r}.readU32()"), + WireType::Prim(Prim::I64) | WireType::Prim(Prim::U64) | WireType::Handle(_) => { + format!("{r}.readI64()") + } + WireType::Prim(Prim::F32) => format!("{r}.readF32()"), + WireType::Prim(Prim::F64) => format!("{r}.readF64()"), WireType::Enum(name) => format!("{}.fromValue({r}.readI32())", local_type_name(name)), - WireType::String => format!("{r}.readString()"), - WireType::Bytes => format!("{r}.readBytes()"), + WireType::Prim(Prim::String) => format!("{r}.readString()"), + WireType::Prim(Prim::Bytes) => format!("{r}.readBytes()"), WireType::User(name) => format!("unpack{}({r})", local_type_name(name)), WireType::Optional(inner) => { format!("{r}.readOptional {{ {} }}", kt_read_expr(inner, r)) @@ -83,20 +86,20 @@ pub(crate) fn kt_read_expr(t: &TypeRef, r: &str) -> String { /// The Kotlin expression packing the public value `expr` of type `t` into a /// freshly encoded `ByteArray`. -pub(crate) fn kt_encode_expr(t: &TypeRef, expr: &str) -> String { +pub(crate) fn kt_encode_expr(t: &Ty, expr: &str) -> String { format!("weaveEncode {{ w -> {} }}", kt_write_expr(t, "w", expr, 0)) } /// The Kotlin expression decoding the `ByteArray` expression `expr` into the /// public value of type `t`, rejecting malformed or trailing bytes. -pub(crate) fn kt_decode_expr(t: &TypeRef, expr: &str) -> String { +pub(crate) fn kt_decode_expr(t: &Ty, expr: &str) -> String { format!("weaveDecode({expr}) {{ r -> {} }}", kt_read_expr(t, "r")) } /// The Kotlin statement writing field `field` of the value `v` (a record or /// rich-enum variant) into the writer `w`, spelling the property access with /// the keyword-escaped field name. -pub(crate) fn kt_write_field(ty: &TypeRef, w: &str, field: &str) -> String { +pub(crate) fn kt_write_field(ty: &Ty, w: &str, field: &str) -> String { kt_write_expr(ty, w, &format!("v.{}", kt_escape(field)), 0) } @@ -113,11 +116,11 @@ pub(crate) fn model_uses_buffers(model: &BindingModel) -> bool { .is_some_and(|e| e.codes.iter().any(|c| !c.fields.is_empty())) || m.callbacks .iter() - .any(|cb| cb.params.iter().any(|p| abi::is_buffered(&p.ty))) + .any(|cb| cb.params.iter().any(|p| p.ty.is_buffered())) || m.callables().any(|f| { - f.params.iter().any(|p| abi::is_buffered(&p.ty)) - || f.ret.as_ref().is_some_and(abi::is_buffered) - || matches!(&f.shape, CallShape::Iterator(it) if abi::is_buffered(&it.elem)) + f.params.iter().any(|p| p.ty.is_buffered()) + || f.ret.as_ref().is_some_and(Ty::is_buffered) + || matches!(&f.shape, CallShape::Iterator(it) if it.elem.is_buffered()) }) }) } diff --git a/crates/weaveffi-gen-android/src/entities.rs b/crates/weaveffi-gen-android/src/entities.rs index 183606a3..524b09d9 100644 --- a/crates/weaveffi-gen-android/src/entities.rs +++ b/crates/weaveffi-gen-android/src/entities.rs @@ -2,16 +2,15 @@ //! exception hierarchy, interface wrapper classes, and the lazy iterator //! classes, plus the per-type buffer codecs. -use weaveffi_core::abi; use weaveffi_core::codegen::common::pascal_case; use weaveffi_core::codegen::CodeWriter; use weaveffi_core::errors; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ BindingModel, EnumBinding, ErrorBinding, FnBinding, InterfaceBinding, IteratorBinding, StructBinding, }; use weaveffi_core::utils::local_type_name; -use weaveffi_ir::ir::TypeRef; use crate::calls::{ interface_native_call, interface_native_decl, interface_native_name, kotlin_error_mapper, @@ -344,18 +343,18 @@ pub(crate) fn render_kotlin_struct_codecs(out: &mut String, s: &StructBinding) { /// The Kotlin expression converting a boxed element pulled from `nativeNext` /// (typed `Any`, spelled `raw`) into the iterator's public element type. -pub(crate) fn kotlin_iter_elem_convert(elem: &TypeRef) -> String { +pub(crate) fn kotlin_iter_elem_convert(elem: &Ty) -> String { // A buffered element crosses as a packed `ByteArray`: decode it into the // idiomatic Kotlin value. - if abi::is_buffered(elem) { + if elem.is_buffered() { return kt_decode_expr(elem, "(raw as ByteArray)"); } match elem { - TypeRef::Enum(name) => format!("{}.fromValue(raw as Int)", local_type_name(name)), - TypeRef::Interface(name) => format!("{}(raw as Long)", local_type_name(name)), + Ty::Enum(name) => format!("{}.fromValue(raw as Int)", local_type_name(name)), + Ty::Interface(name) => format!("{}(raw as Long)", local_type_name(name)), // Only `Interface?` reaches here: 0L crosses for none. - TypeRef::Optional(inner) => { - let TypeRef::Interface(name) = inner.as_ref() else { + Ty::Optional(inner) => { + let Ty::Interface(name) = inner.as_ref() else { unreachable!("non-interface optionals are buffered") }; format!( diff --git a/crates/weaveffi-gen-android/src/jni.rs b/crates/weaveffi-gen-android/src/jni.rs index a1be0c51..d4a07a61 100644 --- a/crates/weaveffi-gen-android/src/jni.rs +++ b/crates/weaveffi-gen-android/src/jni.rs @@ -4,14 +4,13 @@ use std::fmt::Write as _; -use weaveffi_core::abi; use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ CallShape, CallbackBinding, FnBinding, InterfaceBinding, IteratorBinding, ListenerBinding, ModuleBinding, ParamBinding, }; use weaveffi_core::plan::{self, ArgPass, ElemFree, RetPass}; -use weaveffi_ir::ir::TypeRef; use crate::calls::interface_native_name; use crate::docs::splice; @@ -81,8 +80,8 @@ pub(crate) fn render_jni_sync_export( // Bytes and buffered returns share the `const uint8_t*` + trailing // `size_t* out_len` shape. - let needs_out_len = matches!(f.ret, Some(TypeRef::Bytes | TypeRef::BorrowedBytes)) - || f.ret.as_ref().is_some_and(abi::is_buffered); + let needs_out_len = matches!(f.ret, Some(Ty::Bytes | Ty::BorrowedBytes)) + || f.ret.as_ref().is_some_and(Ty::is_buffered); if needs_out_len { let _ = writeln!(jni_c, " size_t out_len = 0;"); } @@ -200,9 +199,9 @@ pub(crate) fn render_jni_interface( /// the pinned `WeaveContinuation`. Buffered results arrive as an owned /// `(result_ptr, result_len)` pair, copied into a `jbyteArray` the Kotlin /// wrapper decodes and then released with `weaveffi_free_bytes`. -fn write_jni_box_result(out: &mut String, ret: Option<&TypeRef>) { +fn write_jni_box_result(out: &mut String, ret: Option<&Ty>) { let mut w = CodeWriter::four_space().with_depth(2); - if ret.is_some_and(abi::is_buffered) { + if ret.is_some_and(Ty::is_buffered) { w.line("jbyteArray boxed = (*env)->NewByteArray(env, (jsize)result_len);"); w.line("if (boxed && result_ptr) { (*env)->SetByteArrayRegion(env, boxed, 0, (jsize)result_len, (const jbyte*)result_ptr); }"); w.line("weaveffi_free_bytes((uint8_t*)result_ptr, result_len);"); @@ -218,54 +217,54 @@ fn write_jni_box_result(out: &mut String, ret: Option<&TypeRef>) { None => { w.line("jobject boxed = NULL;"); } - Some(TypeRef::I8 | TypeRef::U8) => { + Some(Ty::I8 | Ty::U8) => { w.line("jclass boxCls = (*env)->FindClass(env, \"java/lang/Byte\");"); w.line("jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, \"valueOf\", \"(B)Ljava/lang/Byte;\");"); w.line("jobject boxed = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, (jbyte)result);"); } - Some(TypeRef::I16 | TypeRef::U16) => { + Some(Ty::I16 | Ty::U16) => { w.line("jclass boxCls = (*env)->FindClass(env, \"java/lang/Short\");"); w.line("jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, \"valueOf\", \"(S)Ljava/lang/Short;\");"); w.line("jobject boxed = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, (jshort)result);"); } - Some(TypeRef::I32 | TypeRef::Enum(_)) => { + Some(Ty::I32 | Ty::Enum(_)) => { w.line("jclass boxCls = (*env)->FindClass(env, \"java/lang/Integer\");"); w.line("jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, \"valueOf\", \"(I)Ljava/lang/Integer;\");"); w.line("jobject boxed = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, (jint)result);"); } - Some(TypeRef::U32 | TypeRef::I64 | TypeRef::U64 | TypeRef::Handle) => { + Some(Ty::U32 | Ty::I64 | Ty::U64 | Ty::Handle) => { w.line("jclass boxCls = (*env)->FindClass(env, \"java/lang/Long\");"); w.line("jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, \"valueOf\", \"(J)Ljava/lang/Long;\");"); w.line("jobject boxed = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, (jlong)result);"); } // A typed handle or owned interface result arrives as a pointer slot; // the boxed `Long` carries the pointer bits for the wrapper to adopt. - Some(TypeRef::TypedHandle(_) | TypeRef::Interface(_)) => { + Some(Ty::TypedHandle(_) | Ty::Interface(_)) => { w.line("jclass boxCls = (*env)->FindClass(env, \"java/lang/Long\");"); w.line("jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, \"valueOf\", \"(J)Ljava/lang/Long;\");"); w.line("jobject boxed = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, (jlong)(intptr_t)result);"); } - Some(TypeRef::F64) => { + Some(Ty::F64) => { w.line("jclass boxCls = (*env)->FindClass(env, \"java/lang/Double\");"); w.line("jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, \"valueOf\", \"(D)Ljava/lang/Double;\");"); w.line("jobject boxed = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, (jdouble)result);"); } - Some(TypeRef::F32) => { + Some(Ty::F32) => { w.line("jclass boxCls = (*env)->FindClass(env, \"java/lang/Float\");"); w.line("jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, \"valueOf\", \"(F)Ljava/lang/Float;\");"); w.line("jobject boxed = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, (jfloat)result);"); } - Some(TypeRef::Bool) => { + Some(Ty::Bool) => { w.line("jclass boxCls = (*env)->FindClass(env, \"java/lang/Boolean\");"); w.line("jmethodID valueOf = (*env)->GetStaticMethodID(env, boxCls, \"valueOf\", \"(Z)Ljava/lang/Boolean;\");"); w.line("jobject boxed = (*env)->CallStaticObjectMethod(env, boxCls, valueOf, result ? JNI_TRUE : JNI_FALSE);"); } - Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => { + Some(Ty::StringUtf8 | Ty::BorrowedStr) => { // Owned by the consumer: copy, then free. w.line("jobject boxed = result ? (jobject)(*env)->NewStringUTF(env, result) : (jobject)(*env)->NewStringUTF(env, \"\");"); w.line("weaveffi_free_string(result);"); } - Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => { + Some(Ty::Bytes | Ty::BorrowedBytes) => { // Owned by the consumer: copy, then free. w.line("jbyteArray boxed = (*env)->NewByteArray(env, (jsize)result_len);"); w.line("if (boxed && result) { (*env)->SetByteArrayRegion(env, boxed, 0, (jsize)result_len, (const jbyte*)result); }"); @@ -273,23 +272,16 @@ fn write_jni_box_result(out: &mut String, ret: Option<&TypeRef>) { } // Only `Interface?` reaches here (every other optional is buffered): // a nullable owned pointer boxed as `Long`, null crossing as `null`. - Some(TypeRef::Optional(_)) => { + Some(Ty::Optional(_)) => { w.line("jobject boxed = NULL;"); w.block("if (result != NULL) {", "}", |w| { splice(w, |o| { - write_boxed_scalar(o, &TypeRef::Handle, "_opt", "(intptr_t)result", " ") + write_boxed_scalar(o, &Ty::Handle, "_opt", "(intptr_t)result", " ") }); w.line("boxed = _opt;"); }); } - Some( - TypeRef::Record(_) - | TypeRef::RichEnum(_) - | TypeRef::List(_) - | TypeRef::Map(_, _) - | TypeRef::Iterator(_) - | TypeRef::Named(_), - ) => { + Some(Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) | Ty::Iterator(_)) => { unreachable!("buffered results are handled above; iterators cannot be async") } } @@ -451,7 +443,7 @@ fn write_jni_cb_box_arg(out: &mut String, p: &ParamBinding, var: &str) { let slots = &p.abi; let n0 = c_local(&slots[0].name); let mut w = CodeWriter::four_space().with_depth(1); - if abi::is_buffered(&p.ty) { + if p.ty.is_buffered() { let n1 = &slots[1].name; w.line(format!( "jbyteArray {var} = (*env)->NewByteArray(env, (jsize){n1});" @@ -463,27 +455,27 @@ fn write_jni_cb_box_arg(out: &mut String, p: &ParamBinding, var: &str) { return; } match &p.ty { - TypeRef::I8 - | TypeRef::I16 - | TypeRef::I32 - | TypeRef::U8 - | TypeRef::U16 - | TypeRef::U32 - | TypeRef::I64 - | TypeRef::U64 - | TypeRef::F32 - | TypeRef::F64 - | TypeRef::Bool - | TypeRef::Enum(_) - | TypeRef::Handle => { + Ty::I8 + | Ty::I16 + | Ty::I32 + | Ty::U8 + | Ty::U16 + | Ty::U32 + | Ty::I64 + | Ty::U64 + | Ty::F32 + | Ty::F64 + | Ty::Bool + | Ty::Enum(_) + | Ty::Handle => { splice(&mut w, |o| write_boxed_scalar(o, &p.ty, var, &n0, " ")); } - TypeRef::StringUtf8 | TypeRef::BorrowedStr => { + Ty::StringUtf8 | Ty::BorrowedStr => { w.line(format!( "jobject {var} = {n0} ? (jobject)(*env)->NewStringUTF(env, {n0}) : (jobject)(*env)->NewStringUTF(env, \"\");" )); } - TypeRef::Bytes | TypeRef::BorrowedBytes => { + Ty::Bytes | Ty::BorrowedBytes => { let n1 = &slots[1].name; w.line(format!( "jbyteArray {var} = (*env)->NewByteArray(env, (jsize){n1});" @@ -492,20 +484,20 @@ fn write_jni_cb_box_arg(out: &mut String, p: &ParamBinding, var: &str) { "if ({var} && {n0}) {{ (*env)->SetByteArrayRegion(env, {var}, 0, (jsize){n1}, (const jbyte*){n0}); }}" )); } - TypeRef::TypedHandle(_) | TypeRef::Interface(_) => { + Ty::TypedHandle(_) | Ty::Interface(_) => { splice(&mut w, |o| { - write_boxed_scalar(o, &TypeRef::Handle, var, &format!("(intptr_t){n0}"), " ") + write_boxed_scalar(o, &Ty::Handle, var, &format!("(intptr_t){n0}"), " ") }); } // Only `Interface?` reaches here: a nullable borrowed pointer boxed // as `Long`, null crossing as `null`. - TypeRef::Optional(_) => { + Ty::Optional(_) => { w.line(format!("jobject {var} = NULL;")); w.block(format!("if ({n0}) {{"), "}", |w| { splice(w, |o| { write_boxed_scalar( o, - &TypeRef::Handle, + &Ty::Handle, &format!("{var}_box"), &format!("(intptr_t){n0}"), " ", @@ -514,11 +506,10 @@ fn write_jni_cb_box_arg(out: &mut String, p: &ParamBinding, var: &str) { w.line(format!("{var} = {var}_box;")); }); } - TypeRef::Record(_) | TypeRef::RichEnum(_) | TypeRef::List(_) | TypeRef::Map(_, _) => { + Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => { unreachable!("buffered callback arguments are handled above") } - TypeRef::Iterator(_) => unreachable!("validation rejects iterator callback params"), - TypeRef::Named(_) => unreachable!("unresolved type reference"), + Ty::Iterator(_) => unreachable!("validation rejects iterator callback params"), } out.push_str(&w.finish()); } @@ -610,7 +601,7 @@ pub(crate) fn render_jni_listener_fns( ); // When the callback has buffered params, the Kotlin external is the // private `{register}Jni` behind the decoding wrapper. - if cb.params.iter().any(|p| abi::is_buffered(&p.ty)) { + if cb.params.iter().any(|p| p.ty.is_buffered()) { register_kt.push_str("Jni"); } let unregister_kt = kotlin_fn_name( @@ -765,9 +756,9 @@ fn build_c_call_args(args: &mut Vec, p: &ParamBinding, module: &str, c_p // nullable spelling passes the unboxed pointer value acquired above. ArgPass::Object { nullable, .. } => { let iname = match &p.ty { - TypeRef::Interface(iname) => iname, - TypeRef::Optional(inner) => match inner.as_ref() { - TypeRef::Interface(iname) => iname, + Ty::Interface(iname) => iname, + Ty::Optional(inner) => match inner.as_ref() { + Ty::Interface(iname) => iname, _ => unreachable!("non-interface optionals are buffered"), }, _ => unreachable!("object-passed parameters are interfaces"), @@ -780,26 +771,26 @@ fn build_c_call_args(args: &mut Vec, p: &ParamBinding, module: &str, c_p } } ArgPass::Direct { .. } => match &p.ty { - TypeRef::Bool => args.push(format!("(bool)({} == JNI_TRUE)", name)), - TypeRef::I8 => args.push(format!("(int8_t){}", name)), - TypeRef::U8 => args.push(format!("(uint8_t){}", name)), - TypeRef::I16 => args.push(format!("(int16_t){}", name)), - TypeRef::U16 => args.push(format!("(uint16_t){}", name)), - TypeRef::I32 => args.push(format!("(int32_t){}", name)), - TypeRef::U32 => args.push(format!("(uint32_t){}", name)), - TypeRef::I64 => args.push(format!("(int64_t){}", name)), - TypeRef::U64 => args.push(format!("(uint64_t){}", name)), - TypeRef::F32 => args.push(format!("(float){}", name)), - TypeRef::F64 => args.push(format!("(double){}", name)), - TypeRef::Handle => args.push(format!("(weaveffi_handle_t){}", name)), + Ty::Bool => args.push(format!("(bool)({} == JNI_TRUE)", name)), + Ty::I8 => args.push(format!("(int8_t){}", name)), + Ty::U8 => args.push(format!("(uint8_t){}", name)), + Ty::I16 => args.push(format!("(int16_t){}", name)), + Ty::U16 => args.push(format!("(uint16_t){}", name)), + Ty::I32 => args.push(format!("(int32_t){}", name)), + Ty::U32 => args.push(format!("(uint32_t){}", name)), + Ty::I64 => args.push(format!("(int64_t){}", name)), + Ty::U64 => args.push(format!("(uint64_t){}", name)), + Ty::F32 => args.push(format!("(float){}", name)), + Ty::F64 => args.push(format!("(double){}", name)), + Ty::Handle => args.push(format!("(weaveffi_handle_t){}", name)), // A typed handle lowers to the owner-qualified C struct pointer // (mutable receiver), so the cross-module JNI shim must cast // through that pointer rather than the generic integer handle. - TypeRef::TypedHandle(sname) => { + Ty::TypedHandle(sname) => { let c_struct = weaveffi_core::utils::c_abi_struct_name(sname, module, c_prefix); args.push(format!("({}*)(intptr_t){}", c_struct, name)); } - TypeRef::Enum(_) => args.push(format!("(int32_t){}", name)), + Ty::Enum(_) => args.push(format!("(int32_t){}", name)), other => unreachable!("{other:?} is not passed directly"), }, } @@ -831,7 +822,7 @@ fn write_return_handling( jni_c: &mut String, c_sym: &str, call_args: &[String], - returns: Option<&TypeRef>, + returns: Option<&Ty>, params: &[ParamBinding], module: &str, c_prefix: &str, @@ -873,10 +864,10 @@ fn write_return_handling( // returns a nullable owned pointer, boxed for Kotlin's `Long?` as a // `java.lang.Long` or NULL. RetPass::Object { nullable: true, .. } => { - let TypeRef::Optional(inner) = ret_type else { + let Ty::Optional(inner) = ret_type else { unreachable!("nullable object returns are optionals") }; - let TypeRef::Interface(iname) = inner.as_ref() else { + let Ty::Interface(iname) = inner.as_ref() else { unreachable!("non-interface optionals are buffered") }; let c_ty = weaveffi_core::utils::c_abi_struct_name(iname, module, c_prefix); @@ -894,7 +885,7 @@ fn write_return_handling( | RetPass::Object { nullable: false, .. } => match ret_type { - TypeRef::Bool => { + Ty::Bool => { w.line(format!("bool rv = {}({});", c_sym, call_with_err)); splice(&mut w, |o| release_jni_resources(o, params)); splice(&mut w, |o| write_error_check(o, returns, thrower)); @@ -904,7 +895,7 @@ fn write_return_handling( // so the return variable must be that pointer (not the generic // integer handle) and round-trip through `intptr_t`. The untyped // `Handle` case stays in the scalar fallthrough below. - TypeRef::TypedHandle(name) => { + Ty::TypedHandle(name) => { let c_ty = weaveffi_core::utils::c_abi_struct_name(name, module, c_prefix); w.line(format!("{}* rv = {}({});", c_ty, c_sym, call_with_err)); splice(&mut w, |o| release_jni_resources(o, params)); @@ -926,71 +917,71 @@ fn write_return_handling( /// The C declaration type of an iterator's `out_item` pointee, rendered from /// the same lowering the C header uses. -fn iter_item_c_type(elem: &TypeRef, module: &str, c_prefix: &str) -> String { +fn iter_item_c_type(elem: &Ty, module: &str, c_prefix: &str) -> String { weaveffi_core::model::iterator_item_ctype(elem, module).render_c(c_prefix) } /// Box one iterator element scalar `src` (a plain lvalue) into a JVM /// reference `var`. -fn write_boxed_scalar(out: &mut String, ty: &TypeRef, var: &str, src: &str, indent: &str) { +fn write_boxed_scalar(out: &mut String, ty: &Ty, var: &str, src: &str, indent: &str) { let mut w = CodeWriter::four_space().with_depth(indent.len() / 4); match ty { - TypeRef::StringUtf8 | TypeRef::BorrowedStr => { + Ty::StringUtf8 | Ty::BorrowedStr => { w.line(format!( "jstring {v} = {s} ? (*env)->NewStringUTF(env, {s}) : (*env)->NewStringUTF(env, \"\");", v = var, s = src )); } - TypeRef::I8 | TypeRef::U8 => { + Ty::I8 | Ty::U8 => { w.line(format!( "jclass {v}_cls = (*env)->FindClass(env, \"java/lang/Byte\");", v = var )); w.line(format!("jobject {v} = (*env)->CallStaticObjectMethod(env, {v}_cls, (*env)->GetStaticMethodID(env, {v}_cls, \"valueOf\", \"(B)Ljava/lang/Byte;\"), (jbyte){s});", v = var, s = src)); } - TypeRef::I16 | TypeRef::U16 => { + Ty::I16 | Ty::U16 => { w.line(format!( "jclass {v}_cls = (*env)->FindClass(env, \"java/lang/Short\");", v = var )); w.line(format!("jobject {v} = (*env)->CallStaticObjectMethod(env, {v}_cls, (*env)->GetStaticMethodID(env, {v}_cls, \"valueOf\", \"(S)Ljava/lang/Short;\"), (jshort){s});", v = var, s = src)); } - TypeRef::I32 | TypeRef::Enum(_) => { + Ty::I32 | Ty::Enum(_) => { w.line(format!( "jclass {v}_cls = (*env)->FindClass(env, \"java/lang/Integer\");", v = var )); w.line(format!("jobject {v} = (*env)->CallStaticObjectMethod(env, {v}_cls, (*env)->GetStaticMethodID(env, {v}_cls, \"valueOf\", \"(I)Ljava/lang/Integer;\"), (jint){s});", v = var, s = src)); } - TypeRef::U32 | TypeRef::I64 | TypeRef::U64 => { + Ty::U32 | Ty::I64 | Ty::U64 => { w.line(format!( "jclass {v}_cls = (*env)->FindClass(env, \"java/lang/Long\");", v = var )); w.line(format!("jobject {v} = (*env)->CallStaticObjectMethod(env, {v}_cls, (*env)->GetStaticMethodID(env, {v}_cls, \"valueOf\", \"(J)Ljava/lang/Long;\"), (jlong){s});", v = var, s = src)); } - TypeRef::TypedHandle(_) | TypeRef::Handle | TypeRef::Interface(_) => { + Ty::TypedHandle(_) | Ty::Handle | Ty::Interface(_) => { w.line(format!( "jclass {v}_cls = (*env)->FindClass(env, \"java/lang/Long\");", v = var )); w.line(format!("jobject {v} = (*env)->CallStaticObjectMethod(env, {v}_cls, (*env)->GetStaticMethodID(env, {v}_cls, \"valueOf\", \"(J)Ljava/lang/Long;\"), (jlong)(intptr_t){s});", v = var, s = src)); } - TypeRef::F32 => { + Ty::F32 => { w.line(format!( "jclass {v}_cls = (*env)->FindClass(env, \"java/lang/Float\");", v = var )); w.line(format!("jobject {v} = (*env)->CallStaticObjectMethod(env, {v}_cls, (*env)->GetStaticMethodID(env, {v}_cls, \"valueOf\", \"(F)Ljava/lang/Float;\"), (jfloat){s});", v = var, s = src)); } - TypeRef::F64 => { + Ty::F64 => { w.line(format!( "jclass {v}_cls = (*env)->FindClass(env, \"java/lang/Double\");", v = var )); w.line(format!("jobject {v} = (*env)->CallStaticObjectMethod(env, {v}_cls, (*env)->GetStaticMethodID(env, {v}_cls, \"valueOf\", \"(D)Ljava/lang/Double;\"), (jdouble){s});", v = var, s = src)); } - TypeRef::Bool => { + Ty::Bool => { w.line(format!( "jclass {v}_cls = (*env)->FindClass(env, \"java/lang/Boolean\");", v = var @@ -1021,7 +1012,7 @@ fn write_iterator_launch( ) { let args_str = call_args.join(", "); let launch_call = join_call_args(&args_str, "&err"); - let iter_ret = TypeRef::Iterator(Box::new(it.elem.clone())); + let iter_ret = Ty::Iterator(Box::new(it.elem.clone())); let mut w = CodeWriter::four_space().with_depth(1); w.line(format!( @@ -1060,7 +1051,7 @@ pub(crate) fn render_jni_iterator_natives( // Only `Interface?` elements stay a nullable pointer (boxed as 0L for // none); every other optional is buffered. let leaf = match &it.elem { - TypeRef::Optional(inner) => inner.as_ref(), + Ty::Optional(inner) => inner.as_ref(), other => other, }; @@ -1135,7 +1126,7 @@ pub(crate) fn render_jni_iterator_natives( /// Emit the `if (err.code != 0)` check dispatching to `thrower` and exiting /// with the JNI default return for `ret_type`. -fn write_error_check(out: &mut String, ret_type: Option<&TypeRef>, thrower: &str) { +fn write_error_check(out: &mut String, ret_type: Option<&Ty>, thrower: &str) { let mut w = CodeWriter::four_space().with_depth(1); w.block("if (err.code != 0) {", "}", |w| { w.line(format!("{thrower}(env, &err);")); diff --git a/crates/weaveffi-gen-android/src/lib.rs b/crates/weaveffi-gen-android/src/lib.rs index 347d4351..a0299130 100644 --- a/crates/weaveffi-gen-android/src/lib.rs +++ b/crates/weaveffi-gen-android/src/lib.rs @@ -16,8 +16,6 @@ mod entities; mod jni; mod package; mod runtime; -#[cfg(test)] -mod tests; mod types; use camino::Utf8Path; @@ -157,8 +155,6 @@ impl LanguageBackend for AndroidGenerator { } } -weaveffi_core::impl_generator_via_backend!(AndroidGenerator); - /// Render the complete `WeaveFFI.kt`: the `WeaveFFI` entry class (listener /// registration and free functions inside the companion), then the entity /// surface (enums, records, interfaces, iterator classes), the exception diff --git a/crates/weaveffi-gen-android/src/tests.rs b/crates/weaveffi-gen-android/src/tests.rs deleted file mode 100644 index 842974ed..00000000 --- a/crates/weaveffi-gen-android/src/tests.rs +++ /dev/null @@ -1,3841 +0,0 @@ -//! Unit tests for the Android backend: rendering assertions over the Kotlin -//! wrapper and the JNI C bridge, plus file-set and config-variant checks. - -use camino::Utf8Path; -use weaveffi_core::codegen::common::pascal_case; -use weaveffi_core::codegen::Generator; -use weaveffi_core::model::BindingModel; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_ir::ir::{ - Api, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, Module, Param, StructDef, - StructField, TypeRef, -}; - -use crate::types::{jni_param_type, kotlin_jni_type, kotlin_type}; -use crate::{AndroidConfig, AndroidGenerator}; - -fn make_api(modules: Vec) -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: "0.7.0".to_string(), - modules, - generators: None, - package: None, - }) -} - -/// Test-local shim mirroring the driver: build the model once and hand it -/// to the renderer (production code never calls `BindingModel::build`). -fn render_kotlin(api: &ResolvedApi, package: &str, strip: bool, input_basename: &str) -> String { - super::render_kotlin( - &BindingModel::build(api, "weaveffi"), - package, - strip, - input_basename, - ) -} - -/// Test-local shim for the JNI renderer; `c_prefix` seeds the model the -/// same way the driver's global prefix does. -fn render_jni_c( - api: &ResolvedApi, - package: &str, - strip: bool, - input_basename: &str, - c_prefix: &str, -) -> String { - super::render_jni_c( - &BindingModel::build(api, c_prefix), - package, - strip, - input_basename, - ) -} - -fn make_struct_api() -> ResolvedApi { - make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![], - structs: vec![StructDef { - name: "Contact".to_string(), - doc: None, - fields: vec![ - StructField { - name: "name".to_string(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "age".to_string(), - ty: TypeRef::I32, - doc: None, - }, - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]) -} - -fn enum_variant(name: &str, value: i32, fields: Vec) -> EnumVariant { - EnumVariant { - name: name.to_string(), - value, - doc: None, - fields, - } -} - -fn field(name: &str, ty: TypeRef) -> StructField { - StructField { - name: name.to_string(), - ty, - doc: None, - } -} - -/// The `shapes` conformance sample in its already-resolved IR form: a rich -/// (algebraic) enum `Shape`, a plain enum `Channel`, and free functions that -/// take/return the rich enum (lowered to an opaque `Struct` pointer). -fn make_shapes_api() -> ResolvedApi { - make_api(vec![Module { - name: "shapes".to_string(), - enums: vec![ - EnumDef { - name: "Shape".to_string(), - doc: None, - variants: vec![ - enum_variant("Empty", 0, vec![]), - enum_variant("Circle", 1, vec![field("radius", TypeRef::F64)]), - enum_variant( - "Rectangle", - 2, - vec![field("width", TypeRef::F32), field("height", TypeRef::F32)], - ), - enum_variant( - "Labeled", - 3, - vec![ - field("label", TypeRef::StringUtf8), - field("count", TypeRef::U8), - ], - ), - ], - }, - EnumDef { - name: "Channel".to_string(), - doc: None, - variants: vec![ - enum_variant("Red", 0, vec![]), - enum_variant("Green", 1, vec![]), - enum_variant("Blue", 2, vec![]), - ], - }, - ], - // Rich-enum references are resolved to opaque `Struct` pointers. - functions: vec![ - Function { - name: "describe".to_string(), - params: vec![Param { - name: "shape".to_string(), - ty: TypeRef::RichEnum("Shape".into()), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "scale".to_string(), - params: vec![ - Param { - name: "shape".to_string(), - ty: TypeRef::RichEnum("Shape".into()), - mutable: false, - doc: None, - }, - Param { - name: "factor".to_string(), - ty: TypeRef::F64, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::RichEnum("Shape".into())), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "sum_bytes".to_string(), - params: vec![Param { - name: "values".to_string(), - ty: TypeRef::List(Box::new(TypeRef::U8)), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::U64), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - structs: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]) -} - -// --- Rich (algebraic) enum tests --- - -#[test] -fn kotlin_rich_enum_is_sealed_class_not_plain_enum() { - let kt = render_kotlin(&make_shapes_api(), "com.weaveffi", false, "shapes.yml"); - assert!( - kt.contains("sealed class Shape {"), - "rich enum must be a sealed class: {kt}" - ); - // It must NOT degrade into a plain `enum class Shape(...)`, and it has - // no native handle or disposal surface. - assert!( - !kt.contains("enum class Shape("), - "rich enum must not be emitted as a plain enum class: {kt}" - ); - assert!( - !kt.contains("class Shape internal constructor"), - "rich enum must not be a handle-wrapper class: {kt}" - ); - // The plain sibling enum `Channel` is still a normal enum class. - assert!( - kt.contains("enum class Channel(val value: Int) {"), - "plain enum must still be a plain enum class: {kt}" - ); -} - -#[test] -fn kotlin_rich_enum_variant_subtypes() { - let kt = render_kotlin(&make_shapes_api(), "com.weaveffi", false, "shapes.yml"); - for expected in [ - "object Empty : Shape()", - "data class Circle(val radius: Double) : Shape()", - "data class Rectangle(val width: Float, val height: Float) : Shape()", - "data class Labeled(val label: String, val count: Byte) : Shape()", - ] { - assert!(kt.contains(expected), "missing variant `{expected}`: {kt}"); - } -} - -#[test] -fn kotlin_rich_enum_pack_writes_tag_then_fields() { - let kt = render_kotlin(&make_shapes_api(), "com.weaveffi", false, "shapes.yml"); - assert!( - kt.contains("internal fun packShape(w: WeaveBufferWriter, v: Shape) {"), - "missing packShape codec: {kt}" - ); - assert!( - kt.contains("is Shape.Empty -> w.writeI32(0)"), - "unit variant must write only its tag: {kt}" - ); - let circle = kt.split("is Shape.Circle -> {").nth(1).unwrap(); - assert!( - circle.contains("w.writeI32(1)") && circle.contains("w.writeF64(v.radius)"), - "Circle must write tag 1 then its f64 field: {kt}" - ); - let labeled = kt.split("is Shape.Labeled -> {").nth(1).unwrap(); - assert!( - labeled.contains("w.writeI32(3)") - && labeled.contains("w.writeString(v.label)") - && labeled.contains("w.writeI8(v.count)"), - "Labeled must write tag 3 then its fields in order: {kt}" - ); -} - -#[test] -fn kotlin_rich_enum_unpack_dispatches_on_tag() { - let kt = render_kotlin(&make_shapes_api(), "com.weaveffi", false, "shapes.yml"); - assert!( - kt.contains( - "internal fun unpackShape(r: WeaveBufferReader): Shape = when (val tag = r.readI32()) {" - ), - "missing unpackShape codec: {kt}" - ); - for expected in [ - "0 -> Shape.Empty", - "1 -> Shape.Circle(r.readF64())", - "2 -> Shape.Rectangle(r.readF32(), r.readF32())", - "3 -> Shape.Labeled(r.readString(), r.readI8())", - ] { - assert!( - kt.contains(expected), - "missing unpack arm `{expected}`: {kt}" - ); - } - assert!( - kt.contains("unknown Shape tag $tag"), - "unpack must reject unknown tags: {kt}" - ); -} - -#[test] -fn kotlin_rich_enum_has_no_native_surface() { - let kt = render_kotlin(&make_shapes_api(), "com.weaveffi", false, "shapes.yml"); - for forbidden in [ - "nativeNewCircle", - "nativeTag", - "nativeGetCircleRadius", - "Shape.nativeDestroy", - ] { - assert!( - !kt.contains(forbidden), - "rich enums have no C symbols; found `{forbidden}`: {kt}" - ); - } -} - -#[test] -fn kotlin_rich_enum_function_marshalling() { - let kt = render_kotlin(&make_shapes_api(), "com.weaveffi", false, "shapes.yml"); - // A rich enum passed in is packed into a ByteArray; one returned is - // decoded from the ByteArray the JNI shim copies back. - assert!( - kt.contains( - "@JvmStatic fun shapesDescribe(shape: Shape): String = shapesDescribeJni(weaveEncode { w -> packShape(w, shape) })" - ), - "rich-enum param must marshal via packShape: {kt}" - ); - assert!( - kt.contains( - "@JvmStatic fun shapesScale(shape: Shape, factor: Double): Shape = weaveDecode(shapesScaleJni(weaveEncode { w -> packShape(w, shape) }, factor)) { r -> unpackShape(r) }" - ), - "rich-enum return must decode via unpackShape: {kt}" - ); - assert!( - kt.contains( - "@JvmStatic private external fun shapesScaleJni(shape: ByteArray, factor: Double): ByteArray" - ), - "JNI external must carry the rich enum as a ByteArray: {kt}" - ); -} - -#[test] -fn jni_rich_enum_param_pins_and_releases_buffer() { - let jni = render_jni_c( - &make_shapes_api(), - "com.weaveffi", - false, - "shapes.yml", - "weaveffi", - ); - let describe = jni - .split("Java_com_weaveffi_WeaveFFI_shapesDescribeJni") - .nth(1) - .unwrap(); - assert!( - describe.contains("jbyte* shape_elems = (*env)->GetByteArrayElements(env, shape, NULL);"), - "buffered param must pin the ByteArray: {jni}" - ); - assert!( - describe.contains( - "weaveffi_shapes_describe((const uint8_t*)shape_elems, (size_t)shape_len, &err)" - ), - "buffered param must pass borrowed (ptr, len): {jni}" - ); - assert!( - describe.contains("(*env)->ReleaseByteArrayElements(env, shape, shape_elems, JNI_ABORT);"), - "buffered param must be released without copy-back: {jni}" - ); -} - -#[test] -fn jni_rich_enum_return_copies_and_frees_buffer() { - let jni = render_jni_c( - &make_shapes_api(), - "com.weaveffi", - false, - "shapes.yml", - "weaveffi", - ); - let scale = jni - .split("Java_com_weaveffi_WeaveFFI_shapesScaleJni") - .nth(1) - .unwrap(); - assert!( - scale.contains("const uint8_t* rv = weaveffi_shapes_scale((const uint8_t*)shape_elems, (size_t)shape_len, (double)factor, &out_len, &err);"), - "buffered return must thread the out_len slot: {jni}" - ); - assert!( - scale.contains("jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len);"), - "buffered return must copy into a ByteArray: {jni}" - ); - assert!( - scale.contains("weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len);"), - "the producer allocation must be freed after copying: {jni}" - ); -} - -#[test] -fn jni_rich_enum_has_no_object_bridge() { - let jni = render_jni_c( - &make_shapes_api(), - "com.weaveffi", - false, - "shapes.yml", - "weaveffi", - ); - for forbidden in [ - "Java_com_weaveffi_Shape_", - "weaveffi_shapes_Shape_tag", - "weaveffi_shapes_Shape_Circle_new", - "weaveffi_shapes_Shape_destroy", - ] { - assert!( - !jni.contains(forbidden), - "rich enums have no C symbols; found `{forbidden}`: {jni}" - ); - } -} - -#[test] -fn rich_enum_appears_in_generated_files() { - let api = make_shapes_api(); - let dir = tempfile::tempdir().unwrap(); - let out = Utf8Path::from_path(dir.path()).unwrap(); - AndroidGenerator - .generate(&api, out, &AndroidConfig::default()) - .unwrap(); - let kotlin = - std::fs::read_to_string(out.join("android/src/main/kotlin/com/weaveffi/WeaveFFI.kt")) - .unwrap(); - assert!( - kotlin.contains("sealed class Shape {"), - "rich enum sealed class missing from generated Kotlin file" - ); - assert!( - kotlin.contains("internal fun packShape(") && kotlin.contains("internal fun unpackShape("), - "rich enum codecs missing from generated Kotlin file" - ); - let jni = std::fs::read_to_string(out.join("android/src/main/cpp/weaveffi_jni.c")).unwrap(); - assert!( - jni.contains("Java_com_weaveffi_WeaveFFI_scaleJni") - && jni.contains("weaveffi_shapes_scale((const uint8_t*)shape_elems"), - "buffered rich enum marshalling missing from generated JNI file" - ); -} - -#[test] -fn listeners_generate_kotlin_and_jni() { - use weaveffi_ir::ir::{CallbackDef, ListenerDef}; - let api = make_api(vec![Module { - name: "events".to_string(), - functions: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![CallbackDef { - name: "OnMessage".to_string(), - doc: None, - params: vec![Param { - name: "message".to_string(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - }], - listeners: vec![ListenerDef { - name: "message_listener".to_string(), - event_callback: "OnMessage".to_string(), - doc: None, - }], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let kt = render_kotlin(&api, "com.weaveffi", false, "weaveffi.yml"); - assert!( - kt.contains( - "@JvmStatic external fun eventsRegisterMessageListener(callback: (String) -> Unit): Long" - ), - "register external missing: {kt}" - ); - assert!( - kt.contains("@JvmStatic external fun eventsUnregisterMessageListener(id: Long)"), - "unregister external missing: {kt}" - ); - - let jni = render_jni_c(&api, "com.weaveffi", false, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("#include "), - "registry must be mutex-guarded: {jni}" - ); - assert!( - jni.contains("static void weaveffi_events_OnMessage_fn_jni_tramp(const char* message, void* context)"), - "trampoline missing: {jni}" - ); - assert!( - jni.contains("AttachCurrentThread"), - "trampoline must attach producer threads: {jni}" - ); - assert!( - jni.contains("\"invoke\", \"(Ljava/lang/Object;)Ljava/lang/Object;\""), - "must call the erased Function1.invoke: {jni}" - ); - assert!( - jni.contains("Java_com_weaveffi_WeaveFFI_eventsRegisterMessageListener"), - "register JNI export missing: {jni}" - ); - assert!( - jni.contains("weaveffi_events_register_message_listener(weaveffi_events_OnMessage_fn_jni_tramp, ctx)"), - "register must call the C ABI register symbol: {jni}" - ); - assert!( - jni.contains("NewGlobalRef"), - "callback must be pinned with a global ref: {jni}" - ); - assert!( - jni.contains("DeleteGlobalRef"), - "unregister must unpin the callback: {jni}" - ); -} - -#[test] -fn list_of_string_return_is_buffered() { - let api = make_api(vec![Module { - name: "m".to_string(), - functions: vec![Function { - name: "all_names".to_string(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::StringUtf8))), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len);"), - "string-list return crosses as one value buffer: {jni}" - ); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("fun allNames(): List"), - "kotlin surface must be List: {kt}" - ); - assert!( - kt.contains("weaveDecode(allNamesJni()) { r -> r.readList { r.readString() } }"), - "the wrapper must decode the buffered list: {kt}" - ); -} - -/// A single-module API with one free function, for return-marshalling -/// tests. -fn make_fn_api( - name: &str, - params: Vec, - returns: Option, - throws: bool, -) -> ResolvedApi { - make_api(vec![Module { - name: "m".to_string(), - functions: vec![Function { - name: name.to_string(), - params, - returns, - doc: None, - r#async: false, - throws, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]) -} - -#[test] -fn buffered_list_return_frees_producer_buffer() { - let api = make_fn_api( - "all_names", - vec![], - Some(TypeRef::List(Box::new(TypeRef::StringUtf8))), - false, - ); - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("const uint8_t* rv = weaveffi_m_all_names(&out_len, &err);"), - "the buffered return threads the trailing out_len slot: {jni}" - ); - assert!( - jni.contains("weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len);"), - "the producer buffer must be freed after copying: {jni}" - ); -} - -#[test] -fn optional_scalar_return_is_buffered() { - let api = make_fn_api( - "find_age", - vec![], - Some(TypeRef::Optional(Box::new(TypeRef::I64))), - false, - ); - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len);"), - "an optional scalar return crosses as one value buffer: {jni}" - ); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("fun findAge(): Long? = weaveDecode(findAgeJni()) { r -> r.readOptional { r.readI64() } }"), - "the wrapper must decode the optional flag byte plus value: {kt}" - ); -} - -#[test] -fn map_return_is_buffered_and_freed() { - let api = make_fn_api( - "all_scores", - vec![], - Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )), - false, - ); - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("const uint8_t* rv = weaveffi_m_all_scores(&out_len, &err);"), - "the map return crosses as one value buffer: {jni}" - ); - assert!( - jni.contains("weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len);"), - "the producer buffer must be freed after copying: {jni}" - ); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("fun allScores(): Map = weaveDecode(allScoresJni()) { r -> r.readMap({ r.readString() }, { r.readI32() }) }"), - "the wrapper must decode alternating keys and values: {kt}" - ); -} - -#[test] -fn string_param_released_before_error_check() { - let api = make_fn_api( - "check", - vec![Param { - name: "name".to_string(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - Some(TypeRef::I32), - false, - ); - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - let release = jni - .find("ReleaseStringUTFChars") - .expect("string param must be released"); - let err_check = jni - .find("if (err.code != 0)") - .expect("error check must be emitted"); - assert!( - release < err_check, - "the borrowed string must be released before the error check so \ - error paths cannot leak it: {jni}" - ); -} - -#[test] -fn iterator_fn_emits_lazy_kotlin_wrapper() { - let api = make_fn_api( - "stream_names", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - false, - ); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("fun streamNames(): Iterator = MStreamNamesIterator(streamNamesJni())"), - "the public surface must adopt the handle into the iterator class: {kt}" - ); - assert!( - kt.contains("@JvmStatic private external fun streamNamesJni(): Long"), - "the native launcher must return the raw handle: {kt}" - ); - assert!( - kt.contains( - "class MStreamNamesIterator internal constructor(private var handle: Long) : Iterator, java.io.Closeable {" - ), - "missing lazy iterator wrapper class: {kt}" - ); - assert!( - kt.contains("val slot = nativeNext(handle)"), - "hasNext must pull exactly one element into the lookahead slot: {kt}" - ); - assert!( - kt.contains("override fun close() {") && kt.contains("protected fun finalize() {"), - "the iterator must destroy its handle via close()/finalize(): {kt}" - ); - assert!( - !kt.contains("ArrayList") && !kt.contains("toList"), - "the iterator must not drain into a list: {kt}" - ); -} - -#[test] -fn iterator_fn_emits_jni_launch_next_destroy() { - let api = make_fn_api( - "stream_names", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - false, - ); - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("return (jlong)(intptr_t)_iter;"), - "the launcher must hand the raw iterator handle to Kotlin: {jni}" - ); - assert!( - jni.contains("Java_com_weaveffi_MStreamNamesIterator_nativeNext"), - "missing per-iterator nativeNext export: {jni}" - ); - assert!( - jni.contains("Java_com_weaveffi_MStreamNamesIterator_nativeDestroy"), - "missing per-iterator nativeDestroy export: {jni}" - ); - assert!( - jni.contains("weaveffi_free_string(_item);"), - "each string element must be freed after NewStringUTF: {jni}" - ); - assert!( - !jni.contains("java/util/ArrayList") && !jni.contains("while ("), - "the glue must not drain the iterator eagerly: {jni}" - ); -} - -#[test] -fn iterator_record_elements_are_decoded_and_freed() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![Function { - name: "stream_contacts".to_string(), - params: vec![], - returns: Some(TypeRef::Iterator(Box::new(TypeRef::Record( - "Contact".into(), - )))), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".to_string(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("return weaveDecode((raw as ByteArray)) { r -> unpackContact(r) }"), - "record elements must be decoded from the buffered ByteArray: {kt}" - ); - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - let next_start = jni - .find("Java_com_weaveffi_ContactsStreamContactsIterator_nativeNext") - .expect("nativeNext export missing"); - let next_end = jni[next_start..] - .find("\n}\n") - .map(|i| next_start + i) - .expect("nativeNext body must close"); - let next_body = &jni[next_start..next_end]; - assert!( - next_body.contains("jbyteArray _jitem = (*env)->NewByteArray(env, (jsize)_item_len);"), - "buffered elements must be copied into a ByteArray: {next_body}" - ); - assert!( - next_body.contains("weaveffi_free_bytes((uint8_t*)_item, _item_len);"), - "each buffered element must be freed after copying: {next_body}" - ); -} - -#[test] -fn iterator_throws_uses_domain_thrower_per_next() { - let api = make_api(vec![Module { - name: "kv".to_string(), - functions: vec![Function { - name: "scan".to_string(), - params: vec![], - returns: Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - doc: None, - r#async: false, - throws: true, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: Some(ErrorDomain { - name: "KvError".to_string(), - codes: vec![ErrorCode { - name: "IoFailure".to_string(), - code: 1, - message: "IO failure".to_string(), - doc: None, - fields: vec![], - }], - }), - modules: vec![], - }]); - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - let next_start = jni - .find("Java_com_weaveffi_KvScanIterator_nativeNext") - .expect("nativeNext export missing"); - assert!( - jni[next_start..].contains("throw_weaveffi_kv_KvError(env, &err);"), - "per-next errors on a throwing callable must use the typed domain thrower: {jni}" - ); -} - -#[test] -fn listener_exception_policy_routes_to_handler_then_describes() { - use weaveffi_ir::ir::{CallbackDef, ListenerDef}; - let api = make_api(vec![Module { - name: "events".to_string(), - functions: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![CallbackDef { - name: "OnMessage".to_string(), - doc: None, - params: vec![Param { - name: "message".to_string(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - }], - listeners: vec![ListenerDef { - name: "message_listener".to_string(), - event_callback: "OnMessage".to_string(), - doc: None, - }], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("weaveffi_jni_handle_uncaught(env);"), - "the trampoline must route exceptions to the uncaught handler: {jni}" - ); - assert!( - jni.contains("(*env)->ExceptionDescribe(env);"), - "unhandled exceptions must be logged with ExceptionDescribe: {jni}" - ); - assert!( - !jni.contains("if ((*env)->ExceptionCheck(env)) (*env)->ExceptionClear(env);"), - "exceptions must never be silently cleared: {jni}" - ); - assert!( - jni.contains("JNI_OnLoad") - && jni.contains("\"dispatchCallbackException\", \"(Ljava/lang/Throwable;)V\""), - "the handler hook must be cached at load time: {jni}" - ); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("fun setCallbackExceptionHandler(handler: ((Throwable) -> Unit)?)"), - "missing settable exception handler: {kt}" - ); - assert!( - kt.contains("logged with their stack trace and dropped"), - "the listener exception policy must be documented: {kt}" - ); -} - -#[test] -fn async_bytes_result_is_copied_then_freed() { - let api = make_api(vec![Module { - name: "m".to_string(), - functions: vec![Function { - name: "fetch".to_string(), - params: vec![], - returns: Some(TypeRef::Bytes), - doc: None, - r#async: true, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("const uint8_t* result, size_t result_len"), - "the callback signature must match the lowered ABI slots: {jni}" - ); - assert!( - jni.contains("jbyteArray boxed = (*env)->NewByteArray(env, (jsize)result_len);"), - "the owned buffer must be deep-copied into a ByteArray: {jni}" - ); - let cb_start = jni.find("_jni_cb(void* context").expect("callback missing"); - let cb_end = jni[cb_start..] - .find("\n}\n") - .map(|i| cb_start + i) - .expect("callback body must close"); - let cb_body = &jni[cb_start..cb_end]; - assert!( - cb_body.contains("weaveffi_free_bytes((uint8_t*)result, result_len);"), - "the callback owns the result buffer and must free it after copying: {cb_body}" - ); - assert!( - cb_body.contains("weaveffi_error_free(err);"), - "the callback owns the boxed error and must free it: {cb_body}" - ); - assert!( - cb_body.contains("weaveffi_jni_handle_uncaught(env);"), - "resume-path exceptions must go through the uncaught handler: {cb_body}" - ); -} - -#[test] -fn kotlin_struct_is_data_class() { - let api = make_struct_api(); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("data class Contact(val name: String, val age: Int)"), - "missing record data class: {kt}" - ); - assert!( - !kt.contains("class Contact internal constructor"), - "records must not be handle-wrapper classes: {kt}" - ); -} - -#[test] -fn kotlin_struct_codecs_follow_field_order() { - let api = make_struct_api(); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("internal fun packContact(w: WeaveBufferWriter, v: Contact) {"), - "missing packContact codec: {kt}" - ); - let pack = kt.split("internal fun packContact").nth(1).unwrap(); - let name_at = pack.find("w.writeString(v.name)").expect("name write"); - let age_at = pack.find("w.writeI32(v.age)").expect("age write"); - assert!( - name_at < age_at, - "fields must be written in declaration order: {kt}" - ); - assert!( - kt.contains("internal fun unpackContact(r: WeaveBufferReader): Contact = Contact("), - "missing unpackContact codec: {kt}" - ); - let unpack = kt.split("internal fun unpackContact").nth(1).unwrap(); - let name_read = unpack.find("r.readString(),").expect("name read"); - let age_read = unpack.find("r.readI32(),").expect("age read"); - assert!( - name_read < age_read, - "fields must be read in declaration order: {kt}" - ); -} - -#[test] -fn kotlin_struct_has_no_native_surface() { - let api = make_struct_api(); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - for forbidden in [ - "nativeCreate", - "nativeGetName", - "ContactBuilder", - "fun create(name: String", - ] { - assert!( - !kt.contains(forbidden), - "records have no C symbols or builders; found `{forbidden}`: {kt}" - ); - } - let contact_at = kt.find("data class Contact").expect("record class"); - let brand_at = kt.find("open class WeaveFFIException").expect("brand"); - assert!( - !kt[contact_at..brand_at].contains("close()"), - "records need no disposal: {kt}" - ); -} - -#[test] -fn jni_struct_has_no_object_bridge() { - let api = make_struct_api(); - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - for forbidden in [ - "Java_com_weaveffi_Contact_", - "weaveffi_contacts_Contact_create", - "weaveffi_contacts_Contact_destroy", - "weaveffi_contacts_Contact_get_name", - ] { - assert!( - !jni.contains(forbidden), - "records have no C symbols; found `{forbidden}`: {jni}" - ); - } -} - -#[test] -fn kotlin_struct_with_bytes_field() { - let api = make_api(vec![Module { - name: "storage".to_string(), - functions: vec![], - structs: vec![StructDef { - name: "Blob".to_string(), - doc: None, - fields: vec![StructField { - name: "data".to_string(), - ty: TypeRef::Bytes, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("data class Blob(val data: ByteArray)"), - "missing bytes-typed record property: {kt}" - ); - assert!( - kt.contains("w.writeBytes(v.data)"), - "bytes field must serialize as a length-prefixed run: {kt}" - ); - assert!( - kt.contains("r.readBytes(),"), - "bytes field must deserialize via readBytes: {kt}" - ); -} - -#[test] -fn kotlin_struct_with_nested_struct_field() { - let api = make_api(vec![Module { - name: "geo".to_string(), - functions: vec![], - structs: vec![StructDef { - name: "Line".to_string(), - doc: None, - fields: vec![StructField { - name: "start".to_string(), - ty: TypeRef::Record("Point".into()), - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("data class Line(val start: Point)"), - "missing nested record property: {kt}" - ); - assert!( - kt.contains("packPoint(w, v.start)"), - "nested record must serialize inline through its own codec: {kt}" - ); - assert!( - kt.contains("unpackPoint(r),"), - "nested record must deserialize through its own codec: {kt}" - ); -} - -#[test] -fn kotlin_type_for_struct_returns_name() { - assert_eq!(kotlin_type(&TypeRef::Record("Contact".into())), "Contact"); -} - -#[test] -fn kotlin_jni_type_for_struct_is_byte_array() { - assert_eq!( - kotlin_jni_type(&TypeRef::Record("Contact".into())), - "ByteArray" - ); -} - -#[test] -fn pascal_case_converts_snake_case() { - assert_eq!(pascal_case("first_name"), "FirstName"); - assert_eq!(pascal_case("name"), "Name"); - assert_eq!(pascal_case("is_active"), "IsActive"); -} - -#[test] -fn function_with_struct_param_jni() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![Function { - name: "save".to_string(), - params: vec![Param { - name: "contact".to_string(), - ty: TypeRef::Record("Contact".into()), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("@JvmStatic private external fun saveJni(contact: ByteArray)"), - "the JNI external must take the packed ByteArray: {kt}" - ); - assert!( - kt.contains( - "fun save(contact: Contact) { saveJni(weaveEncode { w -> packContact(w, contact) }) }" - ), - "the wrapper must pack the record before crossing: {kt}" - ); - - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains( - "weaveffi_contacts_save((const uint8_t*)contact_elems, (size_t)contact_len, &err)" - ), - "the buffered param must cross as borrowed (ptr, len): {jni}" - ); -} - -#[test] -fn function_returning_struct_jni() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![Function { - name: "create".to_string(), - params: vec![Param { - name: "age".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("const uint8_t* rv = weaveffi_contacts_create((int32_t)age, &out_len, &err);"), - "buffered record return must thread out_len: {jni}" - ); - assert!( - jni.contains("jbyteArray out = (*env)->NewByteArray(env, (jsize)out_len);"), - "buffered record return must copy into a ByteArray: {jni}" - ); - assert!( - jni.contains("weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len);"), - "the producer allocation must be freed: {jni}" - ); -} - -// --- Enum tests --- - -#[test] -fn kotlin_enum_class_generated() { - let api = make_api(vec![Module { - name: "paint".to_string(), - functions: vec![], - structs: vec![], - enums: vec![EnumDef { - name: "Color".to_string(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".to_string(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".to_string(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Blue".to_string(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("enum class Color(val value: Int) {"), - "missing enum class: {kt}" - ); - assert!(kt.contains("Red(0),"), "missing Red variant: {kt}"); - assert!(kt.contains("Green(1),"), "missing Green variant: {kt}"); - assert!( - kt.contains("Blue(2);"), - "missing Blue variant (with semicolon): {kt}" - ); - assert!( - kt.contains("companion object {"), - "missing companion object: {kt}" - ); - assert!( - kt.contains("fun fromValue(value: Int): Color"), - "missing fromValue: {kt}" - ); -} - -#[test] -fn kotlin_type_for_enum_returns_name() { - assert_eq!(kotlin_type(&TypeRef::Enum("Color".into())), "Color"); -} - -#[test] -fn kotlin_jni_type_for_enum_is_int() { - assert_eq!(kotlin_jni_type(&TypeRef::Enum("Color".into())), "Int"); -} - -#[test] -fn function_with_enum_param_kotlin() { - let api = make_api(vec![Module { - name: "paint".to_string(), - functions: vec![Function { - name: "set_color".to_string(), - params: vec![Param { - name: "color".to_string(), - ty: TypeRef::Enum("Color".into()), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("color: Color"), - "public wrapper should use enum class name: {kt}" - ); - assert!( - kt.contains("private external fun setColorJni(color: Int)"), - "native function should use Int for JNI: {kt}" - ); - assert!( - kt.contains("color.value"), - "wrapper should call .value on enum param: {kt}" - ); -} - -#[test] -fn kotlin_function_uses_enum_type() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![Function { - name: "add_contact".to_string(), - params: vec![ - Param { - name: "name".to_string(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }, - Param { - name: "contact_type".to_string(), - ty: TypeRef::Enum("ContactType".into()), - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::Enum("ContactType".into())), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("contactType: ContactType"), - "public signature should use enum class name, not Int: {kt}" - ); - assert!( - kt.contains("): ContactType"), - "return type should use enum class name: {kt}" - ); - assert!( - !kt.contains("external fun addContact("), - "public function should not be external: {kt}" - ); - assert!( - kt.contains("private external fun addContactJni("), - "native function should be private: {kt}" - ); - assert!( - kt.contains("contactType.value"), - "wrapper should extract int via .value: {kt}" - ); - assert!( - kt.contains("ContactType.fromValue("), - "wrapper should wrap return in fromValue: {kt}" - ); -} - -#[test] -fn function_with_enum_param_jni() { - let api = make_api(vec![Module { - name: "paint".to_string(), - functions: vec![Function { - name: "set_color".to_string(), - params: vec![Param { - name: "color".to_string(), - ty: TypeRef::Enum("Color".into()), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("jint color"), - "missing jint param in JNI: {jni}" - ); - assert!( - jni.contains("(int32_t)color"), - "missing int32_t cast: {jni}" - ); - assert!( - jni.contains("WeaveFFI_setColorJni("), - "JNI function name should carry the camelCase Jni suffix: {jni}" - ); -} - -#[test] -fn function_returning_enum_jni() { - let api = make_api(vec![Module { - name: "paint".to_string(), - functions: vec![Function { - name: "get_color".to_string(), - params: vec![], - returns: Some(TypeRef::Enum("Color".into())), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("JNIEXPORT jint JNICALL"), - "missing jint return in JNI: {jni}" - ); - assert!(jni.contains("(jint)"), "missing jint cast: {jni}"); - assert!( - jni.contains("WeaveFFI_getColorJni("), - "JNI function name should carry the camelCase Jni suffix: {jni}" - ); -} - -// --- Optional tests --- - -#[test] -fn kotlin_type_for_optional_int() { - assert_eq!( - kotlin_type(&TypeRef::Optional(Box::new(TypeRef::I32))), - "Int?" - ); -} - -#[test] -fn kotlin_type_for_optional_string() { - assert_eq!( - kotlin_type(&TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - "String?" - ); -} - -#[test] -fn function_with_optional_int_param_kotlin() { - let api = make_api(vec![Module { - name: "store".to_string(), - functions: vec![Function { - name: "find".to_string(), - params: vec![Param { - name: "id".to_string(), - ty: TypeRef::Optional(Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!(kt.contains("id: Int?"), "missing optional Int? param: {kt}"); -} - -#[test] -fn function_with_optional_int_param_jni() { - let api = make_api(vec![Module { - name: "store".to_string(), - functions: vec![Function { - name: "find".to_string(), - params: vec![Param { - name: "id".to_string(), - ty: TypeRef::Optional(Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("jbyteArray id"), - "optional param must cross as a packed jbyteArray: {jni}" - ); - assert!( - jni.contains("weaveffi_store_find((const uint8_t*)id_elems, (size_t)id_len, &err)"), - "optional param must pass borrowed (ptr, len): {jni}" - ); - assert!( - jni.contains("(*env)->ReleaseByteArrayElements(env, id, id_elems, JNI_ABORT);"), - "the pinned encoding must be released without copy-back: {jni}" - ); -} - -#[test] -fn function_with_optional_string_param_jni() { - let api = make_api(vec![Module { - name: "store".to_string(), - functions: vec![Function { - name: "find_name".to_string(), - params: vec![Param { - name: "query".to_string(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("jbyteArray query"), - "optional string param must cross as a packed jbyteArray: {jni}" - ); - assert!( - jni.contains( - "weaveffi_store_find_name((const uint8_t*)query_elems, (size_t)query_len, &err)" - ), - "optional string param must pass borrowed (ptr, len): {jni}" - ); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("weaveEncode { w -> w.writeOptional(query) { v0 -> w.writeString(v0) } }"), - "the wrapper must write the flag byte plus string: {kt}" - ); -} - -#[test] -fn function_returning_optional_int_jni() { - let api = make_api(vec![Module { - name: "store".to_string(), - functions: vec![Function { - name: "lookup".to_string(), - params: vec![], - returns: Some(TypeRef::Optional(Box::new(TypeRef::I32))), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("JNIEXPORT jbyteArray JNICALL"), - "optional return must cross as a value buffer: {jni}" - ); - assert!( - jni.contains("const uint8_t* rv = weaveffi_store_lookup(&out_len, &err);"), - "optional return must thread out_len: {jni}" - ); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains( - "fun lookup(): Int? = weaveDecode(lookupJni()) { r -> r.readOptional { r.readI32() } }" - ), - "the wrapper must decode the optional value: {kt}" - ); -} - -#[test] -fn function_returning_optional_string_jni() { - let api = make_api(vec![Module { - name: "store".to_string(), - functions: vec![Function { - name: "get_name".to_string(), - params: vec![], - returns: Some(TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("const uint8_t* rv = weaveffi_store_get_name(&out_len, &err);"), - "optional string return crosses as a value buffer: {jni}" - ); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("fun getName(): String? = weaveDecode(getNameJni()) { r -> r.readOptional { r.readString() } }"), - "the wrapper must decode the optional string: {kt}" - ); -} - -// --- List tests --- - -#[test] -fn kotlin_type_for_list_int() { - assert_eq!( - kotlin_type(&TypeRef::List(Box::new(TypeRef::I32))), - "List" - ); -} - -#[test] -fn kotlin_type_for_list_string() { - assert_eq!( - kotlin_type(&TypeRef::List(Box::new(TypeRef::StringUtf8))), - "List" - ); -} - -#[test] -fn kotlin_type_for_list_enum() { - assert_eq!( - kotlin_type(&TypeRef::List(Box::new(TypeRef::Enum("Color".into())))), - "List" - ); -} - -#[test] -fn function_with_list_int_param_kotlin() { - let api = make_api(vec![Module { - name: "batch".to_string(), - functions: vec![Function { - name: "process".to_string(), - params: vec![Param { - name: "ids".to_string(), - ty: TypeRef::List(Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("fun process(ids: List)"), - "the public wrapper takes an idiomatic List: {kt}" - ); - assert!( - kt.contains("processJni(weaveEncode { w -> w.writeList(ids) { v0 -> w.writeI32(v0) } })"), - "the wrapper must pack the list into a value buffer: {kt}" - ); -} - -#[test] -fn function_with_list_int_param_jni() { - let api = make_api(vec![Module { - name: "batch".to_string(), - functions: vec![Function { - name: "process".to_string(), - params: vec![Param { - name: "ids".to_string(), - ty: TypeRef::List(Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("jbyteArray ids"), - "list param must cross as a packed jbyteArray: {jni}" - ); - assert!( - jni.contains("GetByteArrayElements(env, ids, NULL)"), - "missing byte-array pin: {jni}" - ); - assert!( - jni.contains("ReleaseByteArrayElements(env, ids, ids_elems, JNI_ABORT)"), - "missing byte-array release: {jni}" - ); - assert!( - jni.contains("weaveffi_batch_process((const uint8_t*)ids_elems, (size_t)ids_len, &err)"), - "list param must pass borrowed (ptr, len): {jni}" - ); -} - -#[test] -fn function_returning_list_int_jni() { - let api = make_api(vec![Module { - name: "batch".to_string(), - functions: vec![Function { - name: "get_ids".to_string(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::I32))), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("JNIEXPORT jbyteArray JNICALL"), - "list return must cross as a value buffer: {jni}" - ); - assert!( - jni.contains("const uint8_t* rv = weaveffi_batch_get_ids(&out_len, &err);"), - "list return must thread out_len: {jni}" - ); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("fun getIds(): List = weaveDecode(getIdsJni()) { r -> r.readList { r.readI32() } }"), - "the wrapper must decode the buffered list: {kt}" - ); -} - -#[test] -fn jni_param_type_enum_is_jint() { - assert_eq!(jni_param_type(&TypeRef::Enum("Color".into())), "jint"); -} - -#[test] -fn jni_param_type_optional_int_is_buffered() { - assert_eq!( - jni_param_type(&TypeRef::Optional(Box::new(TypeRef::I32))), - "jbyteArray" - ); -} - -#[test] -fn jni_param_type_optional_string_is_buffered() { - assert_eq!( - jni_param_type(&TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - "jbyteArray" - ); -} - -#[test] -fn jni_param_type_optional_interface_is_nullable_pointer() { - assert_eq!( - jni_param_type(&TypeRef::Optional(Box::new(TypeRef::Interface( - "Store".into() - )))), - "jobject" - ); -} - -#[test] -fn jni_param_type_list_int_is_buffered() { - assert_eq!( - jni_param_type(&TypeRef::List(Box::new(TypeRef::I32))), - "jbyteArray" - ); -} - -#[test] -fn jni_param_type_list_long_is_buffered() { - assert_eq!( - jni_param_type(&TypeRef::List(Box::new(TypeRef::I64))), - "jbyteArray" - ); -} - -#[test] -fn generate_android_with_structs_and_enums() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![Function { - name: "get_contact".to_string(), - params: vec![Param { - name: "id".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".to_string(), - doc: None, - fields: vec![ - StructField { - name: "name".to_string(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "email".to_string(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "age".to_string(), - ty: TypeRef::I32, - doc: None, - }, - ], - }], - enums: vec![EnumDef { - name: "Color".to_string(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".to_string(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".to_string(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Blue".to_string(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let tmp = std::env::temp_dir().join("weaveffi_test_android_structs_and_enums"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8"); - - AndroidGenerator - .generate(&api, out_dir, &AndroidConfig::default()) - .unwrap(); - - let kotlin = - std::fs::read_to_string(tmp.join("android/src/main/kotlin/com/weaveffi/WeaveFFI.kt")) - .unwrap(); - - assert!( - kotlin.contains("enum class Color(val value: Int) {"), - "missing enum class: {kotlin}" - ); - assert!(kotlin.contains("Red(0),"), "missing Red variant: {kotlin}"); - assert!( - kotlin.contains("Green(1),"), - "missing Green variant: {kotlin}" - ); - assert!( - kotlin.contains("Blue(2);"), - "missing Blue variant with semicolon: {kotlin}" - ); - assert!( - kotlin.contains("fun fromValue(value: Int): Color"), - "missing fromValue: {kotlin}" - ); - - assert!( - kotlin.contains("data class Contact(val name: String, val email: String, val age: Int)"), - "record must be a data class with typed properties: {kotlin}" - ); - assert!( - kotlin.contains("internal fun packContact(") - && kotlin.contains("internal fun unpackContact("), - "record codecs missing: {kotlin}" - ); - assert!( - !kotlin.contains("nativeCreate") && !kotlin.contains("nativeGet"), - "records must have no native surface: {kotlin}" - ); - assert!( - kotlin.contains( - "fun getContact(id: Int): Contact = weaveDecode(getContactJni(id)) { r -> unpackContact(r) }" - ), - "the wrapper must decode the buffered record return: {kotlin}" - ); - - let jni = std::fs::read_to_string(tmp.join("android/src/main/cpp/weaveffi_jni.c")).unwrap(); - - assert!( - jni.contains( - "const uint8_t* rv = weaveffi_contacts_get_contact((int32_t)id, &out_len, &err);" - ), - "buffered record return must thread out_len: {jni}" - ); - assert!( - jni.contains("weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len);"), - "the producer allocation must be freed after copying: {jni}" - ); - assert!( - !jni.contains("weaveffi_contacts_Contact_"), - "records must expose no per-field C symbols: {jni}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn kotlin_type_for_map() { - assert_eq!( - kotlin_type(&TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32) - )), - "Map" - ); - assert_eq!( - kotlin_type(&TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::F64) - )), - "Map" - ); - assert_eq!( - kotlin_type(&TypeRef::Map( - Box::new(TypeRef::I32), - Box::new(TypeRef::StringUtf8) - )), - "Map" - ); -} - -#[test] -fn function_with_map_param_kotlin() { - let api = make_api(vec![Module { - name: "store".to_string(), - functions: vec![Function { - name: "update_scores".to_string(), - params: vec![Param { - name: "scores".to_string(), - ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("scores: Map"), - "missing Map param: {kt}" - ); -} - -#[test] -fn function_with_map_param_jni() { - let api = make_api(vec![Module { - name: "store".to_string(), - functions: vec![Function { - name: "update_scores".to_string(), - params: vec![Param { - name: "scores".to_string(), - ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("jbyteArray scores"), - "map param must cross as a packed jbyteArray: {jni}" - ); - assert!( - jni.contains("GetByteArrayElements(env, scores, NULL)"), - "missing byte-array pin: {jni}" - ); - assert!( - jni.contains( - "weaveffi_store_update_scores((const uint8_t*)scores_elems, (size_t)scores_len, &err)" - ), - "map param must pass borrowed (ptr, len): {jni}" - ); - assert!( - jni.contains("ReleaseByteArrayElements(env, scores, scores_elems, JNI_ABORT)"), - "missing byte-array release: {jni}" - ); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("weaveEncode { w -> w.writeMap(scores, { k0 -> w.writeString(k0) }, { v0 -> w.writeI32(v0) }) }"), - "the wrapper must pack the map before crossing: {kt}" - ); -} - -#[test] -fn android_build_gradle_has_cmake_config() { - let api = make_api(vec![Module { - name: "math".to_string(), - functions: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let tmp = std::env::temp_dir().join("weaveffi_test_android_build_gradle_cmake"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8"); - - AndroidGenerator - .generate(&api, out_dir, &AndroidConfig::default()) - .unwrap(); - - let gradle = std::fs::read_to_string(tmp.join("android/build.gradle")).unwrap(); - assert!( - gradle.contains("externalNativeBuild"), - "missing externalNativeBuild in build.gradle: {gradle}" - ); - assert!( - gradle.contains("path \"src/main/cpp/CMakeLists.txt\""), - "missing cmake path in build.gradle: {gradle}" - ); - assert!( - gradle.contains("cppFlags \"\""), - "missing cppFlags in build.gradle: {gradle}" - ); - assert!( - gradle.contains("namespace 'com.weaveffi'"), - "missing namespace in build.gradle: {gradle}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn function_returning_map_jni() { - let api = make_api(vec![Module { - name: "store".to_string(), - functions: vec![Function { - name: "get_scores".to_string(), - params: vec![], - returns: Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("JNIEXPORT jbyteArray JNICALL"), - "map return must cross as a value buffer: {jni}" - ); - assert!( - jni.contains("const uint8_t* rv = weaveffi_store_get_scores(&out_len, &err);"), - "map return must thread out_len: {jni}" - ); - assert!( - jni.contains("weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len);"), - "the producer allocation must be freed after copying: {jni}" - ); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains( - "fun getScores(): Map = weaveDecode(getScoresJni()) { r -> r.readMap({ r.readString() }, { r.readI32() }) }" - ), - "the wrapper must decode the buffered map: {kt}" - ); -} - -#[test] -fn android_custom_package() { - let api = make_api(vec![Module { - name: "math".to_string(), - functions: vec![Function { - name: "add".to_string(), - params: vec![ - Param { - name: "a".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let config = AndroidConfig { - package: Some("com.mycompany.ffi".into()), - ..AndroidConfig::default() - }; - - let tmp = std::env::temp_dir().join("weaveffi_test_android_custom_package"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8"); - - AndroidGenerator.generate(&api, out_dir, &config).unwrap(); - - let kotlin_path = tmp.join("android/src/main/kotlin/com/mycompany/ffi/WeaveFFI.kt"); - assert!( - kotlin_path.exists(), - "Kotlin file not at custom package path" - ); - - let kotlin = std::fs::read_to_string(&kotlin_path).unwrap(); - assert!( - kotlin.contains("package com.mycompany.ffi"), - "missing custom package declaration: {kotlin}" - ); - assert!( - !kotlin.contains("package com.weaveffi"), - "should not contain default package: {kotlin}" - ); - - let gradle = std::fs::read_to_string(tmp.join("android/build.gradle")).unwrap(); - assert!( - gradle.contains("namespace 'com.mycompany.ffi'"), - "missing custom namespace in build.gradle: {gradle}" - ); - - let jni = std::fs::read_to_string(tmp.join("android/src/main/cpp/weaveffi_jni.c")).unwrap(); - assert!( - jni.contains("Java_com_mycompany_ffi_WeaveFFI_add"), - "missing custom JNI prefix: {jni}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -/// One module declaring an error domain, with one throwing and one -/// non-throwing function, shared by the typed-error tests. -fn make_error_api() -> ResolvedApi { - make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![ - Function { - name: "get".to_string(), - params: vec![Param { - name: "id".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - throws: true, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "count".to_string(), - params: vec![], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: Some(ErrorDomain { - name: "ContactError".to_string(), - codes: vec![ - ErrorCode { - name: "ContactNotFound".to_string(), - code: 1001, - message: "Contact not found".to_string(), - doc: None, - fields: vec![], - }, - ErrorCode { - name: "InvalidInput".to_string(), - code: 1002, - message: "Invalid input provided".to_string(), - doc: None, - fields: vec![], - }, - ], - }), - modules: vec![], - }]) -} - -#[test] -fn kotlin_inline_error_types() { - let kt = render_kotlin(&make_error_api(), "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains( - "open class WeaveFFIException(val code: Int, message: String) : Exception(message)" - ), - "missing open generic exception: {kt}" - ); - assert!( - kt.contains("sealed class ContactException(code: Int, message: String) : WeaveFFIException(code, message) {"), - "missing sealed domain exception: {kt}" - ); - assert!( - kt.contains("class ContactNotFound(message: String = \"Contact not found\") : ContactException(1001, message)"), - "missing ContactNotFound subclass: {kt}" - ); - assert!( - kt.contains("class InvalidInput(message: String = \"Invalid input provided\") : ContactException(1002, message)"), - "missing InvalidInput subclass: {kt}" - ); - assert!( - kt.contains( - "fun fromCode(code: Int, message: String, payload: ByteArray?): WeaveFFIException = when (code) {" - ), - "missing fromCode factory: {kt}" - ); - assert!( - kt.contains("1001 -> ContactNotFound(message)"), - "fromCode must map 1001: {kt}" - ); - assert!( - kt.contains("else -> WeaveFFIException(code, message)"), - "fromCode must fall back to the generic exception: {kt}" - ); -} - -#[test] -fn kotlin_error_payload_fields_decode() { - let mut api = make_error_api().api().clone(); - api.modules[0].errors.as_mut().unwrap().codes[0].fields = vec![ - field("contact_id", TypeRef::I64), - field("hint", TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - ]; - let api = ResolvedApi::assume_resolved(api); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains( - "class ContactNotFound(message: String = \"Contact not found\", val contact_id: Long, val hint: String?) : ContactException(1001, message)" - ), - "payload fields must be constructor properties: {kt}" - ); - assert!( - kt.contains( - "1001 -> if (payload != null) weaveDecode(payload) { r -> ContactNotFound(message, r.readI64(), r.readOptional { r.readString() }) } else WeaveFFIException(code, message)" - ), - "fromCode must decode the payload in declaration order: {kt}" - ); -} - -#[test] -fn jni_typed_error_throwers() { - let jni = render_jni_c( - &make_error_api(), - "com.weaveffi", - true, - "weaveffi.yml", - "weaveffi", - ); - // The generic thrower constructs the brand exception with (code, message). - assert!( - jni.contains("static void throw_weaveffi_error(JNIEnv* env, weaveffi_error* err) {"), - "missing generic thrower: {jni}" - ); - assert!( - jni.contains("FindClass(env, \"com/weaveffi/WeaveFFIException\")"), - "generic thrower must construct the brand exception: {jni}" - ); - assert!( - jni.contains("\"\", \"(ILjava/lang/String;)V\""), - "generic thrower must pass the raw code: {jni}" - ); - // The domain thrower maps known codes to typed subclasses. - assert!( - jni.contains( - "static void throw_weaveffi_contacts_ContactError(JNIEnv* env, weaveffi_error* err) {" - ), - "missing domain thrower: {jni}" - ); - assert!( - jni.contains( - "GetStaticMethodID(env, exClass, \"fromCode\", \"(ILjava/lang/String;[B)Lcom/weaveffi/WeaveFFIException;\")" - ), - "domain thrower must resolve the fromCode factory: {jni}" - ); - assert!( - jni.contains("SetByteArrayRegion(env, jpayload, 0, (jsize)err->payload_len, (const jbyte*)err->payload_ptr)"), - "domain thrower must copy the payload into a jbyteArray: {jni}" - ); - assert!( - jni.contains( - "CallStaticObjectMethod(env, exClass, fromCode, (jint)err->code, jmsg, jpayload)" - ), - "domain thrower must dispatch through fromCode: {jni}" - ); - assert!( - jni.contains("throw_weaveffi_error(env, err);"), - "an unresolvable factory must fall back to the generic thrower: {jni}" - ); - assert!( - jni.contains("weaveffi_error_clear(err);"), - "the thrower must release the message and payload: {jni}" - ); -} - -#[test] -fn jni_throws_split_picks_thrower_per_function() { - let jni = render_jni_c( - &make_error_api(), - "com.weaveffi", - true, - "weaveffi.yml", - "weaveffi", - ); - let get_body = jni - .split("Java_com_weaveffi_WeaveFFI_get(") - .nth(1) - .expect("get export"); - let get_body = &get_body[..get_body.find("\nJNIEXPORT").unwrap_or(get_body.len())]; - assert!( - get_body.contains("throw_weaveffi_contacts_ContactError(env, &err);"), - "throwing function must dispatch to the domain thrower: {jni}" - ); - let count_body = jni - .split("Java_com_weaveffi_WeaveFFI_count(") - .nth(1) - .expect("count export"); - let count_body = &count_body[..count_body.find("\nJNIEXPORT").unwrap_or(count_body.len())]; - assert!( - count_body.contains("throw_weaveffi_error(env, &err);"), - "non-throwing function must dispatch to the generic thrower: {jni}" - ); - assert!( - !count_body.contains("throw_weaveffi_contacts_ContactError"), - "non-throwing function must not use the domain thrower: {jni}" - ); -} - -#[test] -fn android_strip_module_prefix() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![Function { - name: "create_contact".to_string(), - params: vec![Param { - name: "name".to_string(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - // Stripping is the default: the config's `Default` must strip, and the - // emitted Kotlin name is the bare lowerCamelCase function name. - let config = AndroidConfig::default(); - assert!( - config.strip_module_prefix, - "strip_module_prefix must default to true" - ); - - let tmp = std::env::temp_dir().join("weaveffi_test_android_strip_prefix"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - AndroidGenerator.generate(&api, out_dir, &config).unwrap(); - - let kotlin = - std::fs::read_to_string(tmp.join("android/src/main/kotlin/com/weaveffi/WeaveFFI.kt")) - .unwrap(); - - assert!( - kotlin.contains("fun createContact("), - "stripped name should be createContact: {kotlin}" - ); - assert!( - !kotlin.contains("fun contactsCreateContact("), - "should not contain module-prefixed name: {kotlin}" - ); - - let jni = std::fs::read_to_string(tmp.join("android/src/main/cpp/weaveffi_jni.c")).unwrap(); - - assert!( - jni.contains("weaveffi_contacts_create_contact"), - "C ABI call should still use full name: {jni}" - ); - - let no_strip = AndroidConfig { - strip_module_prefix: false, - ..AndroidConfig::default() - }; - let tmp2 = std::env::temp_dir().join("weaveffi_test_android_no_strip_prefix"); - let _ = std::fs::remove_dir_all(&tmp2); - std::fs::create_dir_all(&tmp2).unwrap(); - let out_dir2 = Utf8Path::from_path(&tmp2).expect("valid UTF-8"); - - AndroidGenerator - .generate(&api, out_dir2, &no_strip) - .unwrap(); - - let kotlin2 = - std::fs::read_to_string(tmp2.join("android/src/main/kotlin/com/weaveffi/WeaveFFI.kt")) - .unwrap(); - - assert!( - kotlin2.contains("fun contactsCreateContact("), - "opting out must keep the module-prefixed name: {kotlin2}" - ); - - let _ = std::fs::remove_dir_all(&tmp); - let _ = std::fs::remove_dir_all(&tmp2); -} - -#[test] -fn android_deeply_nested_optional() { - let api = make_api(vec![Module { - name: "edge".into(), - functions: vec![Function { - name: "process".into(), - params: vec![Param { - name: "data".into(), - ty: TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional( - Box::new(TypeRef::Record("Contact".into())), - ))))), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let kotlin = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kotlin.contains("data: List?"), - "should contain deeply nested optional type: {kotlin}" - ); - assert!( - kotlin.contains( - "w.writeOptional(data) { v0 -> w.writeList(v0) { v1 -> w.writeOptional(v1) { v2 -> packContact(w, v2) } } }" - ), - "nested optionals must pack recursively: {kotlin}" - ); -} - -#[test] -fn android_map_of_lists() { - let api = make_api(vec![Module { - name: "edge".into(), - functions: vec![Function { - name: "process".into(), - params: vec![Param { - name: "scores".into(), - ty: TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::List(Box::new(TypeRef::I32))), - ), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let kotlin = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kotlin.contains("scores: Map>"), - "should contain map of lists type: {kotlin}" - ); -} - -#[test] -fn android_enum_keyed_map() { - let api = make_api(vec![Module { - name: "edge".into(), - functions: vec![Function { - name: "process".into(), - params: vec![Param { - name: "contacts".into(), - ty: TypeRef::Map( - Box::new(TypeRef::Enum("Color".into())), - Box::new(TypeRef::Record("Contact".into())), - ), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![EnumDef { - name: "Color".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Blue".into(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let kotlin = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kotlin.contains("contacts: Map"), - "should contain enum-keyed map type: {kotlin}" - ); - assert!( - kotlin.contains( - "w.writeMap(contacts, { k0 -> w.writeI32(k0.value) }, { v0 -> packContact(w, v0) })" - ), - "enum keys pack as their raw value; record values recurse: {kotlin}" - ); -} - -#[test] -fn android_typed_handle_type() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "get_info".into(), - params: vec![Param { - name: "contact".into(), - ty: TypeRef::TypedHandle("Contact".into()), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("contact: Long"), - "TypedHandle is an opaque u64 token surfacing as Long: {kt}" - ); -} - -#[test] -fn android_no_double_free_on_error() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "find_contact".into(), - params: vec![Param { - name: "name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains("GetStringUTFChars"), - "input StringUtf8 should use GetStringUTFChars: {jni}" - ); - assert!( - jni.contains("ReleaseStringUTFChars"), - "input StringUtf8 should release JVM chars: {jni}" - ); - assert!( - !jni.contains("weaveffi_free_string(name"), - "input string param must not be freed via WeaveFFI: {jni}" - ); - - let start = jni - .find("Java_com_weaveffi_WeaveFFI_findContactJni") - .expect("find_contact JNI symbol"); - let rest = &jni[start..]; - let end = rest.find("\nJNIEXPORT ").unwrap_or(rest.len()); - let fn_body = &rest[..end]; - let release_pos = fn_body - .find("ReleaseStringUTFChars") - .expect("borrowed param released after the call"); - let err_pos = fn_body - .find("if (err.code != 0)") - .expect("error check before using return value"); - let free_pos = fn_body - .find("weaveffi_free_bytes((uint8_t*)rv, (size_t)out_len);") - .expect("buffered return freed after copying"); - assert!( - release_pos < err_pos && err_pos < free_pos, - "release, then err check, then copy-and-free; the error path must not free: {jni}" - ); - assert!( - fn_body.contains("throw_weaveffi_error"), - "error path should throw: {jni}" - ); - - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("data class Contact"), - "record data class Contact: {kt}" - ); -} - -#[test] -fn android_custom_prefix_threads_to_c_symbols() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "greet".into(), - params: vec![Param { - name: "name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "myffi"); - - // The JNI C shim must call the user C symbol with the custom C ABI - // prefix, and include the matching C header `myffi.h`. - assert!( - jni.contains("myffi_contacts_greet("), - "shim should call custom-prefixed user C symbol: {jni}" - ); - assert!( - jni.contains("#include \"myffi.h\""), - "shim should include the custom C header: {jni}" - ); - // The default-prefixed user C symbol must NOT leak into the shim. - assert!( - !jni.contains("weaveffi_contacts_greet"), - "default-prefixed user C symbol must not appear: {jni}" - ); - // JNI export names are package-derived (not C-ABI-prefixed) and stay - // literal regardless of the C ABI prefix. - assert!( - jni.contains("Java_com_weaveffi_WeaveFFI_greet"), - "JNI export name must stay package-derived: {jni}" - ); - // Runtime helpers keep the literal `weaveffi_` runtime prefix. - assert!( - jni.contains("weaveffi_error"), - "runtime weaveffi_error helper must remain literal: {jni}" - ); - assert!( - jni.contains("weaveffi_free_string"), - "runtime weaveffi_free_string helper must remain literal: {jni}" - ); -} - -#[test] -fn android_null_check_on_optional_return() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "find_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Optional(Box::new(TypeRef::Record( - "Contact".into(), - )))), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - assert!( - jni.contains( - "const uint8_t* rv = weaveffi_contacts_find_contact((int32_t)id, &out_len, &err);" - ), - "optional record return must cross as a value buffer: {jni}" - ); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains( - "fun findContact(id: Int): Contact? = weaveDecode(findContactJni(id)) { r -> r.readOptional { unpackContact(r) } }" - ), - "the wrapper must decode the optional flag byte: {kt}" - ); -} - -#[test] -fn kotlin_async_function_is_suspend() { - let api = make_api(vec![Module { - name: "tasks".to_string(), - functions: vec![Function { - name: "run".to_string(), - params: vec![Param { - name: "id".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: None, - r#async: true, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("suspend fun"), - "async function should generate suspend fun: {kt}" - ); - assert!( - kt.contains("suspend fun run(id: Int): Int"), - "suspend fun should have correct signature: {kt}" - ); -} - -#[test] -fn kotlin_async_uses_coroutine() { - let api = make_api(vec![Module { - name: "tasks".to_string(), - functions: vec![Function { - name: "run".to_string(), - params: vec![Param { - name: "id".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: None, - r#async: true, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("suspendCancellableCoroutine"), - "async function should use suspendCancellableCoroutine: {kt}" - ); - assert!( - kt.contains("WeaveContinuation"), - "async function should use WeaveContinuation: {kt}" - ); - assert!( - kt.contains("import kotlinx.coroutines.suspendCancellableCoroutine"), - "should import suspendCancellableCoroutine: {kt}" - ); -} - -/// JNI requires `NewGlobalRef` on the Kotlin continuation so it survives -/// across the C-side thread spawn, balanced by `DeleteGlobalRef` in the -/// JNI callback after the suspend point is resumed. The `malloc` of the -/// callback context must also be balanced by `free(ctx)`. -#[test] -fn android_async_pins_callback_for_lifetime() { - let api = make_api(vec![Module { - name: "tasks".into(), - functions: vec![Function { - name: "run".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: None, - r#async: true, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let c = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - let pin_count = c.matches("NewGlobalRef(env, callback)").count(); - let unpin_count = c.matches("DeleteGlobalRef(env, ctx->callback)").count(); - let malloc_count = c.matches("malloc(sizeof(weaveffi_jni_async_ctx))").count(); - let free_count = c.matches("free(ctx);").count(); - assert_eq!( - pin_count, 1, - "expected one NewGlobalRef per async fn, got {pin_count}: {c}" - ); - assert_eq!( - unpin_count, 1, - "expected one DeleteGlobalRef per async fn, got {unpin_count}: {c}" - ); - // One allocation; two textual frees because the attach-failure early - // return must also release the context (each runtime path frees once). - assert_eq!( - malloc_count, 1, - "expected one ctx malloc per async fn, got {malloc_count}: {c}" - ); - assert_eq!( - free_count, 2, - "expected a free on both the completion and attach-failure paths, got {free_count}: {c}" - ); - // The producer thread must not stay attached after completion. - assert!( - c.contains("DetachCurrentThread"), - "async completion must detach the producer thread: {c}" - ); -} - -fn doc_api() -> ResolvedApi { - make_api(vec![Module { - name: "docs".into(), - functions: vec![Function { - name: "do_thing".into(), - params: vec![Param { - name: "x".into(), - ty: TypeRef::I32, - mutable: false, - doc: Some("the input value".into()), - }], - returns: Some(TypeRef::I32), - doc: Some("Performs a thing.".into()), - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Item".into(), - doc: Some("An item we track.".into()), - fields: vec![StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: Some("Stable id".into()), - }], - }], - enums: vec![EnumDef { - name: "Kind".into(), - doc: Some("Kind of item.".into()), - variants: vec![EnumVariant { - name: "Small".into(), - value: 0, - doc: Some("A small one".into()), - fields: vec![], - }], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: Some(ErrorDomain { - name: "DocsErrors".into(), - codes: vec![ErrorCode { - name: "not_found".into(), - code: 1, - message: "Not found".into(), - doc: Some("Raised when missing".into()), - fields: vec![], - }], - }), - modules: vec![], - }]) -} - -#[test] -fn android_emits_doc_on_function() { - let kt = render_kotlin(&doc_api(), "com.weaveffi", true, "weaveffi.yml"); - assert!(kt.contains("Performs a thing."), "{kt}"); -} - -#[test] -fn android_emits_doc_on_struct() { - let kt = render_kotlin(&doc_api(), "com.weaveffi", true, "weaveffi.yml"); - assert!(kt.contains("/** An item we track. */"), "{kt}"); -} - -#[test] -fn android_emits_doc_on_enum_variant() { - let kt = render_kotlin(&doc_api(), "com.weaveffi", true, "weaveffi.yml"); - assert!(kt.contains("/** Kind of item. */"), "{kt}"); - assert!(kt.contains("/** A small one */"), "{kt}"); -} - -#[test] -fn android_emits_doc_on_field() { - let kt = render_kotlin(&doc_api(), "com.weaveffi", true, "weaveffi.yml"); - assert!(kt.contains("/** Stable id */"), "{kt}"); -} - -#[test] -fn android_emits_doc_on_param() { - let kt = render_kotlin(&doc_api(), "com.weaveffi", true, "weaveffi.yml"); - assert!(kt.contains("@param x the input value"), "{kt}"); -} - -/// A `kv` module with a `Store` interface exercising every member shape: -/// the `new` constructor, a named factory, sync methods (throwing and -/// not), an async throwing method, a static, and an interface-typed -/// parameter and return. -fn make_interface_api() -> ResolvedApi { - use weaveffi_ir::ir::InterfaceDef; - make_api(vec![Module { - name: "kv".to_string(), - functions: vec![Function { - name: "merge".to_string(), - params: vec![ - Param { - name: "left_store".to_string(), - ty: TypeRef::Interface("Store".to_string()), - mutable: false, - doc: None, - }, - Param { - name: "right_store".to_string(), - ty: TypeRef::Interface("Store".to_string()), - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::Interface("Store".to_string())), - doc: None, - r#async: false, - throws: true, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![InterfaceDef { - name: "Store".to_string(), - doc: Some("A key-value store.".to_string()), - constructors: vec![ - Function { - name: "new".to_string(), - params: vec![Param { - name: "path".to_string(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "open_readonly".to_string(), - params: vec![Param { - name: "path".to_string(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: true, - cancellable: false, - deprecated: None, - since: None, - }, - ], - methods: vec![ - Function { - name: "get".to_string(), - params: vec![Param { - name: "key".to_string(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - r#async: false, - throws: true, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "len".to_string(), - params: vec![], - returns: Some(TypeRef::U64), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "fetch".to_string(), - params: vec![Param { - name: "key".to_string(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - r#async: true, - throws: true, - cancellable: false, - deprecated: None, - since: None, - }, - ], - statics: vec![Function { - name: "default_path".to_string(), - params: vec![], - returns: Some(TypeRef::StringUtf8), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - }], - errors: Some(ErrorDomain { - name: "KvError".to_string(), - codes: vec![ErrorCode { - name: "KeyNotFound".to_string(), - code: 100, - message: "Key not found".to_string(), - doc: None, - fields: vec![], - }], - }), - modules: vec![], - }]) -} - -#[test] -fn kotlin_interface_class_shape() { - let kt = render_kotlin(&make_interface_api(), "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains( - "class Store internal constructor(internal var handle: Long) : java.io.Closeable {" - ), - "missing handle-backed Closeable class: {kt}" - ); - assert!( - kt.contains("@JvmStatic private external fun nativeDestroy(handle: Long)"), - "missing destroy external: {kt}" - ); - assert!( - kt.contains("override fun close() {") && kt.contains("nativeDestroy(handle)"), - "close() must call the destroy symbol: {kt}" - ); - assert!( - kt.contains("protected fun finalize() {"), - "missing finalizer safety net: {kt}" - ); -} - -#[test] -fn kotlin_interface_constructors_and_statics() { - let kt = render_kotlin(&make_interface_api(), "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("operator fun invoke(path: String): Store = Store(nativeNew(path))"), - "the new constructor must become operator fun invoke: {kt}" - ); - assert!( - kt.contains("fun openReadonly(path: String): Store = Store(nativeOpenReadonly(path))"), - "named constructors must become companion factories: {kt}" - ); - assert!( - kt.contains("fun defaultPath(): String = nativeDefaultPath()"), - "statics must become companion functions: {kt}" - ); -} - -#[test] -fn kotlin_interface_methods() { - let kt = render_kotlin(&make_interface_api(), "com.weaveffi", true, "weaveffi.yml"); - assert!( - kt.contains("fun get(key: String): String = nativeGet(handle, key)"), - "methods must pass the handle as the leading native argument: {kt}" - ); - assert!( - kt.contains("suspend fun fetch(key: String): String = suspendCancellableCoroutine"), - "async methods must be suspend funs: {kt}" - ); - assert!( - kt.contains( - "nativeFetchAsync(handle, key, WeaveContinuation(cont) { code, message, payload -> KvException.fromCode(code, message, payload) })" - ), - "async throwing methods must map errors through the typed domain: {kt}" - ); -} - -#[test] -fn kotlin_interface_params_and_returns() { - let kt = render_kotlin(&make_interface_api(), "com.weaveffi", true, "weaveffi.yml"); - // Interface-typed parameters accept the class and pass the raw handle; - // interface returns re-wrap the owned pointer. Parameter names are - // camelCased from the IR's snake_case. - assert!( - kt.contains( - "@JvmStatic fun merge(leftStore: Store, rightStore: Store): Store = Store(mergeJni(leftStore.handle, rightStore.handle))" - ), - "interface params must unwrap handles and returns must re-wrap: {kt}" - ); -} - -#[test] -fn jni_interface_bridge_members() { - let jni = render_jni_c( - &make_interface_api(), - "com.weaveffi", - true, - "weaveffi.yml", - "weaveffi", - ); - assert!( - jni.contains("JNIEXPORT jlong JNICALL Java_com_weaveffi_Store_nativeNew(JNIEnv* env, jclass clazz, jstring path)"), - "missing constructor export: {jni}" - ); - assert!( - jni.contains("weaveffi_kv_Store_new(path_chars, &err)"), - "constructor must call the lowered ABI symbol: {jni}" - ); - assert!( - jni.contains("JNIEXPORT jstring JNICALL Java_com_weaveffi_Store_nativeGet(JNIEnv* env, jclass clazz, jlong selfHandle, jstring key)"), - "missing method export with leading self slot: {jni}" - ); - assert!( - jni.contains( - "weaveffi_kv_Store_get((const weaveffi_kv_Store*)(intptr_t)selfHandle, key_chars, &err)" - ), - "method must pass the receiver as the leading ABI argument: {jni}" - ); - assert!( - jni.contains("weaveffi_kv_Store_default_path(&err)"), - "static must call its ABI symbol: {jni}" - ); - assert!( - jni.contains("JNIEXPORT void JNICALL Java_com_weaveffi_Store_nativeDestroy(JNIEnv* env, jclass clazz, jlong handle)") - && jni.contains("weaveffi_kv_Store_destroy((weaveffi_kv_Store*)(intptr_t)handle);"), - "missing destroy export: {jni}" - ); - assert!( - jni.contains("JNIEXPORT void JNICALL Java_com_weaveffi_Store_nativeFetchAsync"), - "missing async method launcher: {jni}" - ); - assert!( - jni.contains( - "weaveffi_kv_Store_fetch_async((const weaveffi_kv_Store*)(intptr_t)selfHandle, key_chars, weaveffi_kv_Store_fetch_jni_cb, ctx);" - ), - "async method must forward the receiver to the ABI launcher: {jni}" - ); -} - -/// Generate the Android and C outputs for the shipped sample IDLs through -/// the same parse-validate-generate pipeline the CLI drives, writing into -/// the conformance harness's expected layout -/// (`target/conformance-gen//{android,c}`). Serves two purposes: -/// it smoke-tests generation against the real sample surfaces (interfaces, -/// typed errors, iterators, listeners, records, rich enums, async), and it -/// lets the -/// Kotlin conformance lanes run when the full CLI is blocked by other -/// in-flight generator crates. Skips silently when the samples are not -/// present (for example in a packaged crate). -#[test] -fn samples_generate_android_and_c_outputs() { - let root = Utf8Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap() - .to_path_buf(); - let genroot = root.join("target/conformance-gen"); - for sample in ["events", "kvstore", "shapes"] { - let idl = root.join(format!("samples/{sample}/{sample}.yml")); - if !idl.as_std_path().exists() { - return; - } - let contents = std::fs::read_to_string(idl.as_std_path()).unwrap(); - let api = weaveffi_ir::parse::parse_api_str(&contents, "yaml") - .unwrap_or_else(|e| panic!("parse {sample}: {e}")); - let api = weaveffi_core::validate::validate_api(api, None) - .unwrap_or_else(|e| panic!("validate {sample}: {e:?}")); - let out = genroot.join(sample); - let android_cfg = AndroidConfig { - input_basename: Some(format!("{sample}.yml")), - ..AndroidConfig::default() - }; - AndroidGenerator - .generate(&api, &out, &android_cfg) - .unwrap_or_else(|e| panic!("android generate {sample}: {e}")); - let c_cfg = weaveffi_gen_c::CConfig { - input_basename: Some(format!("{sample}.yml")), - ..Default::default() - }; - weaveffi_gen_c::CGenerator - .generate(&api, &out, &c_cfg) - .unwrap_or_else(|e| panic!("c generate {sample}: {e}")); - assert!( - out.join("android/src/main/kotlin/com/weaveffi/WeaveFFI.kt") - .as_std_path() - .exists(), - "missing Kotlin output for {sample}" - ); - assert!( - out.join("c/weaveffi.h").as_std_path().exists(), - "missing C header for {sample}" - ); - } -} - -#[test] -fn jni_interface_throws_split() { - let jni = render_jni_c( - &make_interface_api(), - "com.weaveffi", - true, - "weaveffi.yml", - "weaveffi", - ); - let get_body = jni - .split("Java_com_weaveffi_Store_nativeGet(") - .nth(1) - .expect("nativeGet export"); - let get_body = &get_body[..get_body.find("\nJNIEXPORT").unwrap_or(get_body.len())]; - assert!( - get_body.contains("throw_weaveffi_kv_KvError(env, &err);"), - "throwing method must use the domain thrower: {jni}" - ); - let len_body = jni - .split("Java_com_weaveffi_Store_nativeLen(") - .nth(1) - .expect("nativeLen export"); - let len_body = &len_body[..len_body.find("\nJNIEXPORT").unwrap_or(len_body.len())]; - assert!( - len_body.contains("throw_weaveffi_error(env, &err);"), - "non-throwing method must use the generic thrower: {jni}" - ); - // Interface params on free functions borrow: the handles are passed - // as const pointers, never destroyed by the bridge. - assert!( - jni.contains("weaveffi_kv_merge((const weaveffi_kv_Store*)(intptr_t)left_store, (const weaveffi_kv_Store*)(intptr_t)right_store, &err)"), - "interface params must be passed as borrowed const pointers: {jni}" - ); -} - -#[test] -fn kotlin_reserved_word_identifiers_are_escaped() { - let api = make_api(vec![Module { - name: "kv".to_string(), - functions: vec![Function { - name: "fun".to_string(), - params: vec![ - Param { - name: "object".to_string(), - ty: TypeRef::I64, - mutable: false, - doc: None, - }, - Param { - name: "when".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Entry".to_string(), - doc: None, - fields: vec![StructField { - name: "val".to_string(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - // The function name and both parameters are Kotlin hard/soft keywords: - // each gains the shared trailing underscore instead of emitting code - // that cannot compile. - assert!( - kt.contains("external fun fun_(object_: Long, when_: Int): Int"), - "reserved-word function and parameter names must be escaped: {kt}" - ); - // A record field named `val` is escaped in the data class property and - // in the pack codec's property access. - assert!( - kt.contains("data class Entry(val val_: String)"), - "reserved-word field names must be escaped in data classes: {kt}" - ); - assert!( - kt.contains("w.writeString(v.val_)"), - "reserved-word field names must be escaped in codecs: {kt}" - ); -} - -#[test] -fn c_reserved_word_parameters_are_escaped_in_jni() { - let api = make_api(vec![Module { - name: "kv".to_string(), - functions: vec![Function { - name: "store".to_string(), - params: vec![Param { - name: "register".to_string(), - ty: TypeRef::I64, - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - // `register` is a C keyword: the JNI shim's parameter declaration and - // every use gain the trailing underscore (linkage is unaffected; the - // export name mangles the Kotlin method name, not C parameter names). - assert!( - jni.contains("jlong register_)"), - "C reserved-word parameters must be escaped in the export signature: {jni}" - ); - assert!( - jni.contains("weaveffi_kv_store((int64_t)register_, &err);"), - "C reserved-word parameters must be escaped at the call site: {jni}" - ); -} - -#[test] -fn error_mapping_covers_declared_codes_only_and_traps_negatives() { - let api = make_api(vec![Module { - name: "kv".to_string(), - functions: vec![ - Function { - name: "load".to_string(), - params: vec![], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - throws: true, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "peek".to_string(), - params: vec![], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - throws: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: Some(ErrorDomain { - name: "KvError".to_string(), - codes: vec![ - ErrorCode { - name: "NotFound".to_string(), - code: 1, - message: "not found".to_string(), - doc: None, - fields: vec![], - }, - ErrorCode { - name: "IoFailure".to_string(), - code: 7, - message: "IO failure".to_string(), - doc: None, - fields: vec![], - }, - ], - }), - modules: vec![], - }]); - let kt = render_kotlin(&api, "com.weaveffi", true, "weaveffi.yml"); - // `fromCode` maps exactly the declared (positive-only) codes to typed - // cases; every other code, including the runtime's reserved negative - // range (-1 generic, -2 panic, -3 marshalling), falls through to the - // generic branded exception. - assert!( - kt.contains("1 -> NotFound(message)") && kt.contains("7 -> IoFailure(message)"), - "declared codes must map to typed exception cases: {kt}" - ); - assert!( - kt.contains("else -> WeaveFFIException(code, message)"), - "unknown and negative codes must fall through to the generic exception: {kt}" - ); - assert!( - !kt.contains("-1 ->") && !kt.contains("-2 ->") && !kt.contains("-3 ->"), - "no reserved negative runtime code may be mapped to a typed case: {kt}" - ); - let jni = render_jni_c(&api, "com.weaveffi", true, "weaveffi.yml", "weaveffi"); - // The throwing callable dispatches to the domain thrower (whose - // `fromCode` still falls back generically for unknown codes); the - // non-throwing callable traps straight through the generic thrower. - let load = jni - .find("Java_com_weaveffi_WeaveFFI_load") - .expect("load export missing"); - let load_end = jni[load..].find("\n}\n").map_or(jni.len(), |e| load + e); - assert!( - jni[load..load_end].contains("throw_weaveffi_kv_KvError(env, &err);"), - "throwing paths must dispatch to the typed domain thrower: {jni}" - ); - let peek = jni - .find("Java_com_weaveffi_WeaveFFI_peek") - .expect("peek export missing"); - let peek_end = jni[peek..].find("\n}\n").map_or(jni.len(), |e| peek + e); - assert!( - jni[peek..peek_end].contains("throw_weaveffi_error(env, &err);"), - "non-throwing paths must trap via the generic thrower: {jni}" - ); -} - -#[test] -fn gradle_string_literals_survive_quotes_and_backslashes() { - use crate::package::{build_gradle, gradle_squote, settings_gradle}; - assert_eq!(gradle_squote(r"it's a \test"), r"it\'s a \\test"); - let settings = settings_gradle("it's-a-lib", "weaveffi.yml"); - assert!( - settings.contains(r"rootProject.name = 'it\'s-a-lib'"), - "a quote in the project name must not terminate the Groovy literal: {settings}" - ); - let gradle = build_gradle("com.weave'ffi", "weaveffi.yml"); - assert!( - gradle.contains(r"namespace 'com.weave\'ffi'"), - "a quote in the package must not terminate the namespace literal: {gradle}" - ); -} diff --git a/crates/weaveffi-gen-android/src/types.rs b/crates/weaveffi-gen-android/src/types.rs index 36691297..1d2016af 100644 --- a/crates/weaveffi-gen-android/src/types.rs +++ b/crates/weaveffi-gen-android/src/types.rs @@ -4,12 +4,11 @@ use std::fmt::Write as _; -use weaveffi_core::abi; use weaveffi_core::codegen::common::pascal_case; use weaveffi_core::lang; +use weaveffi_core::model::Ty; use weaveffi_core::model::{FnBinding, IteratorBinding, ParamBinding}; use weaveffi_core::utils::{local_type_name, wrapper_name}; -use weaveffi_ir::ir::TypeRef; /// Escape a user-chosen identifier for a Kotlin declaration or expression /// position: a reserved word gains the shared trailing underscore @@ -35,53 +34,52 @@ pub(crate) fn c_local(name: &str) -> String { /// The idiomatic (public) Kotlin type for an IR type: records and rich enums /// surface as their generated value classes, lists and maps as `List`/`Map`, /// optionals as nullable types, and handles as raw `Long` tokens. -pub(crate) fn kotlin_type(t: &TypeRef) -> String { +pub(crate) fn kotlin_type(t: &Ty) -> String { match t { - TypeRef::I8 | TypeRef::U8 => "Byte".to_string(), - TypeRef::I16 | TypeRef::U16 => "Short".to_string(), - TypeRef::I32 => "Int".to_string(), - TypeRef::U32 => "Long".to_string(), - TypeRef::I64 | TypeRef::U64 => "Long".to_string(), - TypeRef::F32 => "Float".to_string(), - TypeRef::F64 => "Double".to_string(), - TypeRef::Bool => "Boolean".to_string(), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "String".to_string(), - TypeRef::Bytes | TypeRef::BorrowedBytes => "ByteArray".to_string(), + Ty::I8 | Ty::U8 => "Byte".to_string(), + Ty::I16 | Ty::U16 => "Short".to_string(), + Ty::I32 => "Int".to_string(), + Ty::U32 => "Long".to_string(), + Ty::I64 | Ty::U64 => "Long".to_string(), + Ty::F32 => "Float".to_string(), + Ty::F64 => "Double".to_string(), + Ty::Bool => "Boolean".to_string(), + Ty::StringUtf8 | Ty::BorrowedStr => "String".to_string(), + Ty::Bytes | Ty::BorrowedBytes => "ByteArray".to_string(), // Handles (typed or not) are opaque u64 tokens; both surface as `Long`. - TypeRef::Handle | TypeRef::TypedHandle(_) => "Long".to_string(), + Ty::Handle | Ty::TypedHandle(_) => "Long".to_string(), // An interface surfaces as its generated Kotlin wrapper class; the // JNI layer carries the raw `Long` pointer. - TypeRef::Interface(name) => local_type_name(name).to_string(), + Ty::Interface(name) => local_type_name(name).to_string(), // Records and rich enums are value types: the generated data class or // sealed class, decoded from the value buffer by the wrapper layer. // Cross-module references (e.g. `geo.Point`) name the bare local // Kotlin class `Point`, never the dot-qualified IR name. - TypeRef::Record(name) | TypeRef::RichEnum(name) => local_type_name(name).to_string(), - TypeRef::Enum(name) => local_type_name(name).to_string(), - TypeRef::Optional(inner) => format!("{}?", kotlin_type(inner)), - TypeRef::List(inner) => format!("List<{}>", kotlin_type(inner)), - TypeRef::Iterator(inner) => format!("Iterator<{}>", kotlin_type(inner)), - TypeRef::Map(k, v) => format!("Map<{}, {}>", kotlin_type(k), kotlin_type(v)), - TypeRef::Named(_) => unreachable!("unresolved type reference"), + Ty::Record(name) | Ty::RichEnum(name) => local_type_name(name).to_string(), + Ty::Enum(name) => local_type_name(name).to_string(), + Ty::Optional(inner) => format!("{}?", kotlin_type(inner)), + Ty::List(inner) => format!("List<{}>", kotlin_type(inner)), + Ty::Iterator(inner) => format!("Iterator<{}>", kotlin_type(inner)), + Ty::Map(k, v) => format!("Map<{}, {}>", kotlin_type(k), kotlin_type(v)), } } /// The Kotlin type a value crosses the JNI boundary as: buffered values pack /// into a `ByteArray`, enums cross as their raw `Int`, interfaces and /// iterator handles as raw `Long`s. Everything else matches [`kotlin_type`]. -pub(crate) fn kotlin_jni_type(t: &TypeRef) -> String { - if abi::is_buffered(t) { +pub(crate) fn kotlin_jni_type(t: &Ty) -> String { + if t.is_buffered() { return "ByteArray".to_string(); } match t { - TypeRef::Enum(_) => "Int".to_string(), - TypeRef::Interface(_) => "Long".to_string(), + Ty::Enum(_) => "Int".to_string(), + Ty::Interface(_) => "Long".to_string(), // Only `Interface?` reaches here (every other optional is buffered): // the JNI layer carries the nullable pointer as a boxed `Long?`. - TypeRef::Optional(_) => "Long?".to_string(), + Ty::Optional(_) => "Long?".to_string(), // An iterator crosses JNI as the raw handle returned by the launcher; // the public wrapper adopts it into the generated iterator class. - TypeRef::Iterator(_) => "Long".to_string(), + Ty::Iterator(_) => "Long".to_string(), other => kotlin_type(other), } } @@ -91,16 +89,16 @@ pub(crate) fn kotlin_jni_type(t: &TypeRef) -> String { /// runs, so they surface as their idiomatic value types. Enums stay raw `Int` /// and interfaces raw `Long`: trampolines box arguments on arbitrary producer /// threads where only bootstrap classes (`java/lang/*`) are loadable. -pub(crate) fn kotlin_cb_type(t: &TypeRef) -> String { - if abi::is_buffered(t) { +pub(crate) fn kotlin_cb_type(t: &Ty) -> String { + if t.is_buffered() { return kotlin_type(t); } match t { - TypeRef::Enum(_) => "Int".to_string(), - TypeRef::TypedHandle(_) | TypeRef::Interface(_) => "Long".to_string(), + Ty::Enum(_) => "Int".to_string(), + Ty::TypedHandle(_) | Ty::Interface(_) => "Long".to_string(), // Only `Interface?` reaches here: a nullable raw pointer. - TypeRef::Optional(_) => "Long?".to_string(), - TypeRef::Iterator(_) => unreachable!("validation rejects iterator callback params"), + Ty::Optional(_) => "Long?".to_string(), + Ty::Iterator(_) => unreachable!("validation rejects iterator callback params"), other => kotlin_type(other), } } @@ -108,8 +106,8 @@ pub(crate) fn kotlin_cb_type(t: &TypeRef) -> String { /// The Kotlin parameter types of the JNI-facing listener callback lambda the /// trampoline invokes: buffered arguments arrive as a raw `ByteArray` copy; /// everything else matches [`kotlin_cb_type`]. -pub(crate) fn kotlin_cb_jni_type(t: &TypeRef) -> String { - if abi::is_buffered(t) { +pub(crate) fn kotlin_cb_jni_type(t: &Ty) -> String { + if t.is_buffered() { return "ByteArray".to_string(); } kotlin_cb_type(t) @@ -118,41 +116,37 @@ pub(crate) fn kotlin_cb_jni_type(t: &TypeRef) -> String { /// The JNI C parameter type (`jint`, `jbyteArray`, ...) an IR type crosses /// the JNI export boundary as. Buffered values arrive as one packed /// `jbyteArray`. -pub(crate) fn jni_param_type(t: &TypeRef) -> String { - if abi::is_buffered(t) { +pub(crate) fn jni_param_type(t: &Ty) -> String { + if t.is_buffered() { return "jbyteArray".to_string(); } match t { - TypeRef::I8 | TypeRef::U8 => "jbyte".to_string(), - TypeRef::I16 | TypeRef::U16 => "jshort".to_string(), - TypeRef::I32 | TypeRef::Enum(_) => "jint".to_string(), - TypeRef::U32 - | TypeRef::I64 - | TypeRef::U64 - | TypeRef::TypedHandle(_) - | TypeRef::Handle - | TypeRef::Interface(_) => "jlong".to_string(), - TypeRef::F32 => "jfloat".to_string(), - TypeRef::F64 => "jdouble".to_string(), - TypeRef::Bool => "jboolean".to_string(), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "jstring".to_string(), - TypeRef::Bytes | TypeRef::BorrowedBytes => "jbyteArray".to_string(), + Ty::I8 | Ty::U8 => "jbyte".to_string(), + Ty::I16 | Ty::U16 => "jshort".to_string(), + Ty::I32 | Ty::Enum(_) => "jint".to_string(), + Ty::U32 | Ty::I64 | Ty::U64 | Ty::TypedHandle(_) | Ty::Handle | Ty::Interface(_) => { + "jlong".to_string() + } + Ty::F32 => "jfloat".to_string(), + Ty::F64 => "jdouble".to_string(), + Ty::Bool => "jboolean".to_string(), + Ty::StringUtf8 | Ty::BorrowedStr => "jstring".to_string(), + Ty::Bytes | Ty::BorrowedBytes => "jbyteArray".to_string(), // Only `Interface?` reaches here: Kotlin's `Long?` boxes to // `java.lang.Long`, so the slot is a nullable `jobject`. - TypeRef::Optional(_) => "jobject".to_string(), + Ty::Optional(_) => "jobject".to_string(), // An iterator return crosses as the raw handle (`jlong`); it is never // a parameter (validation rejects that position). - TypeRef::Iterator(_) => "jlong".to_string(), - TypeRef::Record(_) | TypeRef::RichEnum(_) | TypeRef::List(_) | TypeRef::Map(_, _) => { + Ty::Iterator(_) => "jlong".to_string(), + Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => { unreachable!("buffered types are handled above") } - TypeRef::Named(_) => unreachable!("unresolved type reference"), } } /// The JNI C return type of an export: `void` for no return, otherwise the /// same lowering as [`jni_param_type`]. -pub(crate) fn jni_ret_type(t: Option<&TypeRef>) -> String { +pub(crate) fn jni_ret_type(t: Option<&Ty>) -> String { match t { None => "void".to_string(), Some(t) => jni_param_type(t), @@ -162,68 +156,65 @@ pub(crate) fn jni_ret_type(t: Option<&TypeRef>) -> String { /// The C declaration type for the scalar-shaped returns handled by the /// generic fallthrough in `write_return_handling`. Buffered, string, bytes, /// optional, iterator, and typed-handle returns have dedicated emitters. -pub(crate) fn c_type_for_return(t: &TypeRef) -> &'static str { +pub(crate) fn c_type_for_return(t: &Ty) -> &'static str { match t { - TypeRef::I8 => "int8_t", - TypeRef::U8 => "uint8_t", - TypeRef::I16 => "int16_t", - TypeRef::U16 => "uint16_t", - TypeRef::I32 | TypeRef::Enum(_) => "int32_t", - TypeRef::U32 => "uint32_t", - TypeRef::I64 => "int64_t", - TypeRef::U64 => "uint64_t", - TypeRef::F32 => "float", - TypeRef::F64 => "double", - TypeRef::Bool => "bool", - TypeRef::Handle => "weaveffi_handle_t", - TypeRef::Interface(_) => "void*", + Ty::I8 => "int8_t", + Ty::U8 => "uint8_t", + Ty::I16 => "int16_t", + Ty::U16 => "uint16_t", + Ty::I32 | Ty::Enum(_) => "int32_t", + Ty::U32 => "uint32_t", + Ty::I64 => "int64_t", + Ty::U64 => "uint64_t", + Ty::F32 => "float", + Ty::F64 => "double", + Ty::Bool => "bool", + Ty::Handle => "weaveffi_handle_t", + Ty::Interface(_) => "void*", other => unreachable!("return type {other:?} is handled by a dedicated emitter"), } } /// The `return ...;` statement an error path exits an export with, per JNI /// return type. Empty for `void` exports. -pub(crate) fn jni_default_return(t: Option<&TypeRef>) -> &'static str { +pub(crate) fn jni_default_return(t: Option<&Ty>) -> &'static str { let Some(t) = t else { return ""; }; - if abi::is_buffered(t) { + if t.is_buffered() { return "return NULL;"; } match t { - TypeRef::I8 | TypeRef::U8 | TypeRef::I16 | TypeRef::U16 => "return 0;", - TypeRef::I32 | TypeRef::Enum(_) => "return 0;", - TypeRef::U32 | TypeRef::I64 | TypeRef::U64 | TypeRef::TypedHandle(_) | TypeRef::Handle => { - "return 0;" - } - TypeRef::F32 => "return 0.0f;", - TypeRef::F64 => "return 0.0;", - TypeRef::Bool => "return JNI_FALSE;", - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "return NULL;", - TypeRef::Bytes | TypeRef::BorrowedBytes => "return NULL;", - TypeRef::Interface(_) => "return 0;", + Ty::I8 | Ty::U8 | Ty::I16 | Ty::U16 => "return 0;", + Ty::I32 | Ty::Enum(_) => "return 0;", + Ty::U32 | Ty::I64 | Ty::U64 | Ty::TypedHandle(_) | Ty::Handle => "return 0;", + Ty::F32 => "return 0.0f;", + Ty::F64 => "return 0.0;", + Ty::Bool => "return JNI_FALSE;", + Ty::StringUtf8 | Ty::BorrowedStr => "return NULL;", + Ty::Bytes | Ty::BorrowedBytes => "return NULL;", + Ty::Interface(_) => "return 0;", // Only `Interface?` reaches here (a nullable boxed `Long`). - TypeRef::Optional(_) => "return NULL;", + Ty::Optional(_) => "return NULL;", // The iterator launcher returns the handle as a `jlong`; 0 = failed. - TypeRef::Iterator(_) => "return 0;", - TypeRef::Record(_) | TypeRef::RichEnum(_) | TypeRef::List(_) | TypeRef::Map(_, _) => { + Ty::Iterator(_) => "return 0;", + Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => { unreachable!("buffered types are handled above") } - TypeRef::Named(_) => unreachable!("unresolved type reference"), } } /// The C-to-JNI cast prefix for the scalar-shaped returns rendered by the /// generic fallthrough in `write_return_handling`. -pub(crate) fn jni_cast_for(t: &TypeRef) -> &'static str { +pub(crate) fn jni_cast_for(t: &Ty) -> &'static str { match t { - TypeRef::I8 | TypeRef::U8 => "(jbyte)", - TypeRef::I16 | TypeRef::U16 => "(jshort)", - TypeRef::I32 | TypeRef::Enum(_) => "(jint)", - TypeRef::U32 | TypeRef::I64 | TypeRef::U64 | TypeRef::Handle => "(jlong)", - TypeRef::F32 => "(jfloat)", - TypeRef::F64 => "(jdouble)", - TypeRef::TypedHandle(_) | TypeRef::Interface(_) => "(jlong)(intptr_t)", + Ty::I8 | Ty::U8 => "(jbyte)", + Ty::I16 | Ty::U16 => "(jshort)", + Ty::I32 | Ty::Enum(_) => "(jint)", + Ty::U32 | Ty::I64 | Ty::U64 | Ty::Handle => "(jlong)", + Ty::F32 => "(jfloat)", + Ty::F64 => "(jdouble)", + Ty::TypedHandle(_) | Ty::Interface(_) => "(jlong)(intptr_t)", _ => "", } } @@ -293,13 +284,13 @@ pub(crate) fn camel_params(params: &[ParamBinding]) -> Vec { /// and iterator returns (the raw `Long` handle is adopted into the generated /// iterator class). pub(crate) fn needs_wrapper_split(f: &FnBinding) -> bool { - fn differs(t: &TypeRef) -> bool { - abi::is_buffered(t) - || matches!(t, TypeRef::Enum(_) | TypeRef::Interface(_)) - || matches!(t, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Interface(_))) + fn differs(t: &Ty) -> bool { + t.is_buffered() + || matches!(t, Ty::Enum(_) | Ty::Interface(_)) + || matches!(t, Ty::Optional(inner) if matches!(inner.as_ref(), Ty::Interface(_))) } f.params.iter().any(|p| differs(&p.ty)) - || matches!(&f.ret, Some(TypeRef::Iterator(_))) + || matches!(&f.ret, Some(Ty::Iterator(_))) || f.ret.as_ref().is_some_and(differs) } diff --git a/crates/weaveffi-gen-c/src/header.rs b/crates/weaveffi-gen-c/src/header.rs index b1000f11..1222e897 100644 --- a/crates/weaveffi-gen-c/src/header.rs +++ b/crates/weaveffi-gen-c/src/header.rs @@ -14,8 +14,6 @@ use weaveffi_core::utils::{ render_abi_prefix_aliases, render_prelude, render_trailer, CommentStyle, }; -use crate::idents::escape_module_param_names; - /// Render the complete `{prefix}.h` for `api` using `prefix` for every symbol. /// /// Thin wrapper over [`render_c_header_from_model`] for tests and callers @@ -37,17 +35,16 @@ pub fn render_c_header( /// (include guard, includes, prefix aliases, the map-convention comment). The /// C symbol prefix is read from [`BindingModel::prefix`], so every name already /// agrees with the symbols baked into the model. Parameter names that collide -/// with a C keyword are escaped with a trailing underscore before rendering -/// (see the crate's `idents` module). +/// with a C or C++ keyword are escaped inside `cabi` itself (see +/// [`weaveffi_core::cabi::c_param_name`]). pub fn render_c_header_from_model( model: &BindingModel, input_basename: &str, filename: &str, ) -> String { let prefix = model.prefix.as_str(); - let modules = escape_module_param_names(&model.modules); let guard = format!("{}_H", prefix.to_uppercase()); - let mut out = String::with_capacity(2048 + modules.len() * 4096); + let mut out = String::with_capacity(2048 + model.modules.len() * 4096); out.push_str(&render_prelude(CommentStyle::DoubleSlash, input_basename)); let _ = write!(out, "#ifndef {guard}\n#define {guard}\n\n"); out.push_str("#include \n"); @@ -71,7 +68,7 @@ pub fn render_c_header_from_model( )); out.push_str(" */\n\n"); - cabi::render_decls(&mut out, &modules, prefix, true); + cabi::render_decls(&mut out, &model.modules, prefix, true); out.push_str("\n#ifdef __cplusplus\n}\n#endif\n\n"); let _ = write!(out, "#endif // {guard}\n\n"); diff --git a/crates/weaveffi-gen-c/src/idents.rs b/crates/weaveffi-gen-c/src/idents.rs deleted file mode 100644 index 51583ce3..00000000 --- a/crates/weaveffi-gen-c/src/idents.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! C keyword escaping for user-chosen parameter names. -//! -//! IDL parameter names land verbatim in the header's prototypes and callback -//! typedefs, where C reserves words like `register` and `restrict`. C allows -//! omitting parameter names entirely, but the header prints them for -//! readability, so a reserved name must gain the shared trailing-underscore -//! escape (see [`weaveffi_core::lang::escape_ident`]) before it's emitted. -//! Every other identifier in the header carries the symbol prefix and can't -//! collide, so parameter slots are the only position escaped here. - -use weaveffi_core::abi::AbiParam; -use weaveffi_core::lang::{escape_ident, C_KEYWORDS}; -use weaveffi_core::model::{CallShape, FnBinding, ModuleBinding}; - -/// Return a copy of `modules` with every ABI parameter name escaped against -/// the shared C keyword table. -/// -/// Escaping is a no-op for non-reserved names, so the copy renders -/// byte-identically to the input unless the IDL used a C keyword as a -/// parameter name (which previously produced a header that didn't compile). -pub(crate) fn escape_module_param_names(modules: &[ModuleBinding]) -> Vec { - let mut modules = modules.to_vec(); - for module in &mut modules { - for f in &mut module.functions { - escape_fn_param_names(f); - } - for i in &mut module.interfaces { - for f in i - .constructors - .iter_mut() - .chain(i.methods.iter_mut()) - .chain(i.statics.iter_mut()) - { - escape_fn_param_names(f); - } - } - for cb in &mut module.callbacks { - escape_param_names(&mut cb.abi_params); - } - } - modules -} - -/// Escape the ABI parameter names of one callable across all its lowered -/// signatures (sync, async launcher and completion callback, or iterator -/// launch and `next`). -fn escape_fn_param_names(f: &mut FnBinding) { - match &mut f.shape { - CallShape::Sync(abi) => escape_param_names(&mut abi.params), - CallShape::Async(a) => { - escape_param_names(&mut a.launch.params); - escape_param_names(&mut a.callback_params); - } - CallShape::Iterator(it) => { - escape_param_names(&mut it.launch.params); - escape_param_names(&mut it.next.params); - } - } -} - -/// Escape each slot name in `params` against [`C_KEYWORDS`]. -fn escape_param_names(params: &mut [AbiParam]) { - for p in params { - p.name = escape_ident(&p.name, C_KEYWORDS); - } -} diff --git a/crates/weaveffi-gen-c/src/lib.rs b/crates/weaveffi-gen-c/src/lib.rs index b6d93661..f2845b98 100644 --- a/crates/weaveffi-gen-c/src/lib.rs +++ b/crates/weaveffi-gen-c/src/lib.rs @@ -15,11 +15,7 @@ #![warn(clippy::doc_markdown)] mod header; -mod idents; mod package; -#[cfg(test)] -mod tests; - use camino::Utf8Path; use serde::{Deserialize, Serialize}; use weaveffi_core::backend::{LanguageBackend, OutputFile}; @@ -142,5 +138,3 @@ impl LanguageBackend for CGenerator { Some(files) } } - -weaveffi_core::impl_generator_via_backend!(CGenerator); diff --git a/crates/weaveffi-gen-c/src/tests.rs b/crates/weaveffi-gen-c/src/tests.rs deleted file mode 100644 index b4356dbc..00000000 --- a/crates/weaveffi-gen-c/src/tests.rs +++ /dev/null @@ -1,595 +0,0 @@ -//! Tests for the C backend: rendered-header assertions over fixture APIs, -//! keyword-escaping regressions, and packaging checks. - -use camino::Utf8Path; -use weaveffi_core::codegen::Generator; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_ir::ir::Api; -use weaveffi_ir::ir::{ - CallbackDef, EnumDef, EnumVariant, Function, ListenerDef, Module, Param, StructDef, - StructField, TypeRef, -}; - -use super::*; - -#[test] -fn package_bundles_header_libs_and_cmake() { - use weaveffi_core::package::{FileContent, PackageContext}; - use weaveffi_core::platform::{BinarySet, Platform}; - - let api = ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules: vec![module("calc")], - generators: None, - package: None, - }); - let model = BindingModel::build(&api, "weaveffi"); - let mut bins = BinarySet::new("calculator"); - bins.insert(Platform::MacosArm64, "/s/darwin-arm64/libcalculator.dylib"); - bins.insert(Platform::LinuxX64, "/s/linux-x64/libcalculator.so"); - let ctx = PackageContext { - binaries: &bins, - input_basename: Some("calculator.yml"), - }; - let files = LanguageBackend::package( - &CGenerator, - &api, - &model, - &ctx, - Utf8Path::new("/out"), - &CConfig::default(), - ) - .expect("c supports packaging"); - - assert_eq!(files.iter().filter(|f| f.is_binary()).count(), 2); - assert!(files - .iter() - .any(|f| f.path.as_str().ends_with("c/include/weaveffi.h"))); - assert!(files.iter().any(|f| f - .path - .as_str() - .ends_with("c/lib/darwin-arm64/libcalculator.dylib"))); - let cmake = files - .iter() - .find(|f| f.path.as_str().ends_with("c/CMakeLists.txt")) - .expect("CMakeLists present"); - let FileContent::Text(txt) = &cmake.content else { - panic!("CMakeLists is text"); - }; - assert!( - txt.contains("IMPORTED") && txt.contains("calculator::calculator"), - "imported target missing: {txt}" - ); -} - -fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - mutable: false, - doc: None, - } -} - -fn func(name: &str, params: Vec, returns: Option) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - } -} - -fn module(name: &str) -> Module { - Module { - name: name.into(), - functions: vec![], - structs: vec![], - enums: vec![], - interfaces: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - } -} - -fn api(modules: Vec) -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules, - generators: None, - package: None, - }) -} - -fn header(api: &ResolvedApi, prefix: &str) -> String { - render_c_header(api, prefix, "weaveffi.yml", "weaveffi.h") -} - -#[test] -fn emits_guard_and_runtime_decls() { - let h = header(&api(vec![module("math")]), "weaveffi"); - assert!(h.contains("#ifndef WEAVEFFI_H")); - assert!(h.contains("typedef uint64_t weaveffi_handle_t;")); - assert!(h.contains("void weaveffi_free_string(const char* ptr);")); -} - -#[test] -fn typed_handle_target_gets_forward_typedef() { - // A typed handle's target has no declaration of its own (records are - // value types now), so the header must forward-declare its opaque tag - // before any prototype uses it, including across modules. - let shared = Module { - structs: vec![StructDef { - name: "Token".into(), - doc: None, - fields: vec![StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: None, - }], - }], - ..module("shared") - }; - let main = Module { - functions: vec![func( - "open_typed_handle", - vec![], - Some(TypeRef::TypedHandle("shared.Token".into())), - )], - ..module("main") - }; - let h = header(&api(vec![shared, main]), "weaveffi"); - let typedef = "typedef struct weaveffi_shared_Token weaveffi_shared_Token;"; - assert!(h.contains(typedef), "missing tag typedef, header:\n{h}"); - let use_at = h - .find("weaveffi_shared_Token* weaveffi_main_open_typed_handle") - .expect("prototype present"); - assert!( - h.find(typedef).unwrap() < use_at, - "typedef must precede its first use" - ); -} - -#[test] -fn sync_function_signature() { - let m = Module { - functions: vec![func( - "add", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - )], - ..module("math") - }; - let h = header(&api(vec![m]), "weaveffi"); - assert!(h.contains("int32_t weaveffi_math_add(int32_t a, int32_t b, weaveffi_error* out_err);")); -} - -#[test] -fn custom_prefix_is_honored() { - let m = Module { - functions: vec![func("ping", vec![], None)], - ..module("net") - }; - let h = header(&api(vec![m]), "acme"); - assert!(h.contains("#ifndef ACME_H")); - assert!(h.contains("void acme_net_ping(acme_error* out_err);")); - assert!(h.contains("#define acme_error weaveffi_error")); -} - -#[test] -fn visibility_macro_defined_and_applied_to_prototypes() { - let m = Module { - functions: vec![func( - "add", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - )], - ..module("math") - }; - let h = header(&api(vec![m]), "weaveffi"); - // The macro is defined behind an include guard with the three branches. - assert!(h.contains("#ifndef WEAVEFFI_API")); - assert!(h.contains("# define WEAVEFFI_API __declspec(dllexport)")); - assert!(h.contains("# define WEAVEFFI_API __declspec(dllimport)")); - assert!(h.contains("# define WEAVEFFI_API __attribute__((visibility(\"default\")))")); - // Both runtime helpers and user functions carry the export tag. - assert!(h.contains("WEAVEFFI_API void weaveffi_free_string(const char* ptr);")); - assert!(h.contains( - "WEAVEFFI_API int32_t weaveffi_math_add(int32_t a, int32_t b, weaveffi_error* out_err);" - )); - // Type definitions are never tagged: they declare no exported symbol. - assert!(h.contains("typedef uint64_t weaveffi_handle_t;")); - assert!(!h.contains("WEAVEFFI_API typedef")); -} - -#[test] -fn visibility_macro_follows_custom_prefix() { - let m = Module { - functions: vec![func("ping", vec![], None)], - ..module("net") - }; - let h = header(&api(vec![m]), "acme"); - assert!(h.contains("#ifndef ACME_API")); - assert!(h.contains("ifdef ACME_BUILD")); - assert!(h.contains("ACME_API void acme_net_ping(acme_error* out_err);")); - // The default-prefixed macro must not leak when a prefix is configured. - assert!(!h.contains("WEAVEFFI_API")); -} - -#[test] -fn deprecated_uses_portable_macro_not_bare_attribute() { - let m = Module { - functions: vec![Function { - deprecated: Some("use bar instead".into()), - ..func("foo", vec![], None) - }], - ..module("legacy") - }; - let h = header(&api(vec![m]), "weaveffi"); - assert!(h.contains("#ifndef WEAVEFFI_DEPRECATED")); - assert!(h.contains("WEAVEFFI_DEPRECATED(\"use bar instead\")")); - // The message must travel through the macro, never a bare GCC attribute - // (which MSVC cannot parse). - assert!(!h.contains("__attribute__((deprecated(\"use bar instead\")))")); -} - -#[test] -fn record_is_a_value_type_with_no_c_object() { - let m = Module { - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - functions: vec![func( - "save", - vec![param("contact", TypeRef::Record("Contact".into()))], - None, - )], - ..module("contacts") - }; - let h = header(&api(vec![m]), "weaveffi"); - // Records cross the ABI as serialized buffers: no opaque tag, no - // getters, no builder machinery. - assert!(!h.contains("typedef struct weaveffi_contacts_Contact")); - assert!(!h.contains("Contact_get_name")); - assert!(!h.contains("ContactBuilder")); - // A record parameter lowers to a borrowed ptr + len view. - assert!(h.contains( - "void weaveffi_contacts_save(const uint8_t* contact_ptr, size_t contact_len, weaveffi_error* out_err);" - )); -} - -#[test] -fn record_return_uses_out_buffer_params() { - let m = Module { - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - functions: vec![func( - "load", - vec![param("id", TypeRef::I64)], - Some(TypeRef::Record("Contact".into())), - )], - ..module("contacts") - }; - let h = header(&api(vec![m]), "weaveffi"); - assert!( - h.contains( - "const uint8_t* weaveffi_contacts_load(int64_t id, size_t* out_len, weaveffi_error* out_err);" - ), - "expected buffered return with out_len, header was:\n{h}" - ); -} - -#[test] -fn enum_constants() { - let m = Module { - enums: vec![EnumDef { - name: "Color".into(), - doc: None, - variants: vec![EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }], - }], - ..module("gfx") - }; - let h = header(&api(vec![m]), "weaveffi"); - assert!(h.contains("weaveffi_gfx_Color_Red = 0")); -} - -#[test] -fn iterator_emits_next_and_destroy() { - let m = Module { - functions: vec![func( - "get_messages", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - )], - ..module("events") - }; - let h = header(&api(vec![m]), "weaveffi"); - assert!(h.contains( - "weaveffi_events_GetMessagesIterator* weaveffi_events_get_messages(weaveffi_error* out_err);" - )); - assert!(h.contains("weaveffi_events_GetMessagesIterator_next(")); - assert!(h.contains("void weaveffi_events_GetMessagesIterator_destroy(weaveffi_events_GetMessagesIterator* iter);")); -} - -#[test] -fn callback_and_listener() { - let m = Module { - callbacks: vec![CallbackDef { - name: "on_message".into(), - params: vec![param("text", TypeRef::StringUtf8)], - doc: None, - }], - listeners: vec![ListenerDef { - name: "messages".into(), - event_callback: "on_message".into(), - doc: None, - }], - ..module("events") - }; - let h = header(&api(vec![m]), "weaveffi"); - assert!(h.contains( - "typedef void (*weaveffi_events_on_message_fn)(const char* text, void* context);" - )); - assert!(h.contains("uint64_t weaveffi_events_register_messages(weaveffi_events_on_message_fn callback, void* context);")); - assert!(h.contains("void weaveffi_events_unregister_messages(uint64_t id);")); -} - -#[test] -fn interface_emits_tag_members_and_destroy() { - use weaveffi_ir::ir::InterfaceDef; - let m = Module { - interfaces: vec![InterfaceDef { - name: "Store".into(), - doc: Some("A key/value store.".into()), - constructors: vec![Function { - throws: true, - ..func("open", vec![param("path", TypeRef::StringUtf8)], None) - }], - methods: vec![ - func("count", vec![], Some(TypeRef::I64)), - func( - "label", - vec![param("prefix", TypeRef::StringUtf8)], - Some(TypeRef::StringUtf8), - ), - ], - statics: vec![func("default_capacity", vec![], Some(TypeRef::I64))], - }], - ..module("kv") - }; - let h = header(&api(vec![m]), "weaveffi"); - // Opaque tag plus constructor returning an owned pointer. - assert!(h.contains("typedef struct weaveffi_kv_Store weaveffi_kv_Store;")); - assert!(h.contains( - "weaveffi_kv_Store* weaveffi_kv_Store_open(const char* path, weaveffi_error* out_err);" - )); - // Methods carry a leading const self pointer; statics do not. - assert!(h.contains( - "int64_t weaveffi_kv_Store_count(const weaveffi_kv_Store* self, weaveffi_error* out_err);" - )); - assert!(h.contains( - "const char* weaveffi_kv_Store_label(const weaveffi_kv_Store* self, const char* prefix, weaveffi_error* out_err);" - )); - assert!(h.contains("int64_t weaveffi_kv_Store_default_capacity(weaveffi_error* out_err);")); - // The destructor releases the object reference. - assert!(h.contains("void weaveffi_kv_Store_destroy(weaveffi_kv_Store* self);")); -} - -#[test] -fn interface_typed_params_and_returns_are_pointers() { - use weaveffi_ir::ir::InterfaceDef; - let m = Module { - interfaces: vec![InterfaceDef { - name: "Counter".into(), - doc: None, - constructors: vec![func("new", vec![], None)], - methods: vec![func( - "snapshot", - vec![], - Some(TypeRef::Interface("Counter".into())), - )], - statics: vec![], - }], - functions: vec![func( - "read_twice", - vec![param("counter", TypeRef::Interface("Counter".into()))], - Some(TypeRef::I64), - )], - ..module("counters") - }; - let h = header(&api(vec![m]), "weaveffi"); - // A method returning the interface hands back an owned pointer. - assert!(h.contains( - "weaveffi_counters_Counter* weaveffi_counters_Counter_snapshot(const weaveffi_counters_Counter* self, weaveffi_error* out_err);" - )); - // A free function borrows the interface as a const pointer. - assert!(h.contains( - "int64_t weaveffi_counters_read_twice(const weaveffi_counters_Counter* counter, weaveffi_error* out_err);" - )); -} - -#[test] -fn error_domain_emits_code_enum() { - use weaveffi_ir::ir::{ErrorCode, ErrorDomain}; - let m = Module { - errors: Some(ErrorDomain { - name: "KvError".into(), - codes: vec![ - ErrorCode { - name: "KeyNotFound".into(), - code: 1001, - message: "key not found".into(), - doc: None, - fields: vec![], - }, - ErrorCode { - name: "Expired".into(), - code: 1002, - message: "entry expired".into(), - doc: None, - fields: vec![], - }, - ], - }), - functions: vec![Function { - throws: true, - ..func( - "get", - vec![param("key", TypeRef::StringUtf8)], - Some(TypeRef::I64), - ) - }], - ..module("kv") - }; - let h = header(&api(vec![m]), "weaveffi"); - assert!(h.contains("weaveffi_kv_KvError_KeyNotFound = 1001")); - assert!(h.contains("weaveffi_kv_KvError_Expired = 1002")); -} - -#[test] -fn async_emits_callback_typedef_and_launcher() { - let m = Module { - functions: vec![Function { - r#async: true, - cancellable: true, - ..func( - "fetch", - vec![param("id", TypeRef::I64)], - Some(TypeRef::StringUtf8), - ) - }], - ..module("net") - }; - let h = header(&api(vec![m]), "weaveffi"); - assert!(h.contains("typedef void (*weaveffi_net_fetch_callback)(void* context, weaveffi_error* err, const char* result);")); - assert!(h.contains("weaveffi_net_fetch_async(")); - assert!(h.contains("weaveffi_cancel_token* cancel_token")); -} - -#[test] -fn output_files_lists_header_and_source() { - let tmp = std::env::temp_dir().join("weaveffi_c_outfiles"); - let out_dir = Utf8Path::from_path(&tmp).unwrap(); - let files = CGenerator.output_files(&api(vec![module("m")]), out_dir, &CConfig::default()); - assert!(files.iter().any(|f| f.ends_with("c/weaveffi.h"))); - assert!(files.iter().any(|f| f.ends_with("c/weaveffi.c"))); -} - -#[test] -fn reserved_c_keyword_params_are_escaped_in_prototypes() { - // A parameter named after a C keyword previously landed verbatim in the - // prototype (`int32_t register`), which no C compiler accepts. The shared - // escape appends a trailing underscore; non-reserved names pass through. - let m = Module { - functions: vec![func( - "configure", - vec![ - param("register", TypeRef::I32), - param("volatile", TypeRef::StringUtf8), - param("value", TypeRef::I64), - ], - None, - )], - ..module("cfg") - }; - let h = header(&api(vec![m]), "weaveffi"); - assert!( - h.contains( - "void weaveffi_cfg_configure(int32_t register_, const char* volatile_, int64_t value, weaveffi_error* out_err);" - ), - "keyword params must gain a trailing underscore, header was:\n{h}" - ); -} - -#[test] -fn reserved_c_keyword_params_are_escaped_in_callback_typedefs() { - let m = Module { - callbacks: vec![CallbackDef { - name: "on_tick".into(), - params: vec![param("restrict", TypeRef::StringUtf8)], - doc: None, - }], - listeners: vec![ListenerDef { - name: "ticks".into(), - event_callback: "on_tick".into(), - doc: None, - }], - ..module("events") - }; - let h = header(&api(vec![m]), "weaveffi"); - assert!( - h.contains( - "typedef void (*weaveffi_events_on_tick_fn)(const char* restrict_, void* context);" - ), - "callback keyword params must be escaped, header was:\n{h}" - ); -} - -#[test] -fn reserved_c_keyword_params_are_escaped_in_async_and_methods() { - use weaveffi_ir::ir::InterfaceDef; - let m = Module { - functions: vec![Function { - r#async: true, - ..func("fetch", vec![param("switch", TypeRef::I32)], None) - }], - interfaces: vec![InterfaceDef { - name: "Store".into(), - doc: None, - constructors: vec![func("new", vec![], None)], - methods: vec![func("put", vec![param("union", TypeRef::I64)], None)], - statics: vec![], - }], - ..module("kv") - }; - let h = header(&api(vec![m]), "weaveffi"); - assert!( - h.contains("int32_t switch_,"), - "async launcher keyword params must be escaped, header was:\n{h}" - ); - assert!( - h.contains("int64_t union_,"), - "method keyword params must be escaped, header was:\n{h}" - ); -} - -#[test] -fn derived_buffer_slot_names_pass_through_unescaped() { - // A bytes or buffered parameter named after a keyword lowers to derived - // `{name}_ptr` / `{name}_len` slots, which are never keywords themselves - // and must stay untouched. - let m = Module { - functions: vec![func("write", vec![param("int", TypeRef::Bytes)], None)], - ..module("io") - }; - let h = header(&api(vec![m]), "weaveffi"); - assert!( - h.contains("const uint8_t* int_ptr, size_t int_len,"), - "derived slot names must pass through, header was:\n{h}" - ); -} diff --git a/crates/weaveffi-gen-cpp/Cargo.toml b/crates/weaveffi-gen-cpp/Cargo.toml index e2fb5ba3..ec8f56e9 100644 --- a/crates/weaveffi-gen-cpp/Cargo.toml +++ b/crates/weaveffi-gen-cpp/Cargo.toml @@ -11,7 +11,6 @@ categories = ["development-tools::ffi"] [dependencies] weaveffi-core = { path = "../weaveffi-core", version = "0.21.0" } -weaveffi-ir = { path = "../weaveffi-ir", version = "0.21.0" } camino = { workspace = true } heck = { workspace = true } serde = { workspace = true } diff --git a/crates/weaveffi-gen-cpp/src/calls.rs b/crates/weaveffi-gen-cpp/src/calls.rs index 28eb686a..2dc65cbd 100644 --- a/crates/weaveffi-gen-cpp/src/calls.rs +++ b/crates/weaveffi-gen-cpp/src/calls.rs @@ -6,13 +6,13 @@ use heck::{ToSnakeCase, ToUpperCamelCase}; use weaveffi_core::abi; use weaveffi_core::codegen::common::DocCommentStyle; use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ AbiFn, AsyncBinding, CallShape, FnBinding, IteratorBinding, ListenerBinding, ModuleBinding, ParamBinding, }; use weaveffi_core::plan::{elem_free, ret_pass, ArgPass, ElemFree, ErrorStrategy, RetPass}; use weaveffi_core::utils::{c_abi_struct_name, local_type_name}; -use weaveffi_ir::ir::TypeRef; use crate::codec::{emit_read_decl, emit_read_into, emit_write_value}; use crate::types::{ @@ -51,10 +51,10 @@ fn make_error_call(f: &FnBinding, module: &ModuleBinding) -> String { /// The local interface name behind an object-passed type: the interface /// itself, or the interface inside `Interface?`. -fn object_iface_name(ty: &TypeRef) -> &str { +fn object_iface_name(ty: &Ty) -> &str { match ty { - TypeRef::Interface(n) => n, - TypeRef::Optional(inner) => object_iface_name(inner), + Ty::Interface(n) => n, + Ty::Optional(inner) => object_iface_name(inner), _ => unreachable!("object passing only applies to interfaces"), } } @@ -78,6 +78,9 @@ fn emit_param_setup( emit_write_value(w, &p.ty, &name, &buf, 0); vec![format!("{buf}.data()"), format!("{buf}.size()")] } + // `std::string::data()` is the non-const overload on a mutable + // parameter (C++17), yielding the `char*` the write-back slot wants. + ArgPass::String { .. } if p.mutable => vec![format!("{name}.data()")], ArgPass::String { .. } => vec![format!("{name}.c_str()")], ArgPass::Bytes { .. } => { vec![format!("{name}.data()"), format!("{name}.size()")] @@ -96,10 +99,10 @@ fn emit_param_setup( } } ArgPass::Direct { .. } => match &p.ty { - TypeRef::Handle => vec![format!( + Ty::Handle => vec![format!( "static_cast<{prefix}_handle_t>(reinterpret_cast({name}))" )], - TypeRef::Enum(e) => vec![format!( + Ty::Enum(e) => vec![format!( "static_cast<{}>(static_cast({name}))", c_abi_struct_name(e, module, prefix) )], @@ -115,7 +118,7 @@ fn emit_param_setup( /// [`RetPass`] plan. A buffered return decodes the producer's buffer and /// releases it with `{prefix}_free_bytes` via a scope guard, so the release /// happens even when decoding throws. -fn emit_sync_return(w: &mut CodeWriter, ty: &TypeRef, module: &str, prefix: &str) { +fn emit_sync_return(w: &mut CodeWriter, ty: &Ty, module: &str, prefix: &str) { match ret_pass(Some(ty), module, prefix) { RetPass::Buffer => { w.line("detail::BufferGuard result_guard{result, out_len};"); @@ -149,10 +152,10 @@ fn emit_sync_return(w: &mut CodeWriter, ty: &TypeRef, module: &str, prefix: &str )); } RetPass::Direct => match ty { - TypeRef::Handle => { + Ty::Handle => { w.line("return reinterpret_cast(static_cast(result));"); } - TypeRef::Enum(n) => { + Ty::Enum(n) => { w.line(format!( "return static_cast<{}>(result);", local_type_name(n) @@ -277,7 +280,7 @@ fn render_sync_callable( let decls: Vec = f .params .iter() - .map(|p| cpp_param_decl(&p.ty, &cpp_ident(&p.name), &module.path, prefix)) + .map(|p| cpp_param_decl(&p.ty, &cpp_ident(&p.name), p.mutable, &module.path, prefix)) .collect(); let is_ctor = matches!(kind, FnKind::Ctor); @@ -504,7 +507,7 @@ fn render_iterator_callable( let decls: Vec = f .params .iter() - .map(|p| cpp_param_decl(&p.ty, &cpp_ident(&p.name), &module.path, prefix)) + .map(|p| cpp_param_decl(&p.ty, &cpp_ident(&p.name), p.mutable, &module.path, prefix)) .collect(); w.line(format!( "{}{class_name} {cpp_name}({}){} {{", @@ -596,7 +599,7 @@ fn render_iterator_next_method( w.line("return std::nullopt;"); }); w.line("}"); - if abi::is_buffered(&it.elem) { + if it.elem.is_buffered() { // A buffered element is producer-allocated: decode into an owned // value, then release with free_bytes via the scope guard. w.line("detail::BufferGuard item_guard{item, item_len};"); @@ -607,7 +610,7 @@ fn render_iterator_next_method( } else { match (&it.elem, &ef) { // Byte-buffer elements copy then release the producer buffer. - (TypeRef::Bytes | TypeRef::BorrowedBytes, _) => { + (Ty::Bytes | Ty::BorrowedBytes, _) => { w.line("std::vector value(item, item + item_len);"); w.line(format!( "{prefix}_free_bytes(const_cast(item), item_len);" @@ -619,11 +622,11 @@ fn render_iterator_next_method( w.line(format!("{prefix}_free_string(item);")); w.line("return value;"); } - (TypeRef::Enum(n), _) => { + (Ty::Enum(n), _) => { let n = local_type_name(n); w.line(format!("return static_cast<{n}>(item);")); } - (TypeRef::Handle, _) => { + (Ty::Handle, _) => { w.line("return reinterpret_cast(static_cast(item));"); } _ => { @@ -663,7 +666,7 @@ fn render_async_callable( let mut decls: Vec = f .params .iter() - .map(|p| cpp_param_decl(&p.ty, &cpp_ident(&p.name), &module.path, prefix)) + .map(|p| cpp_param_decl(&p.ty, &cpp_ident(&p.name), p.mutable, &module.path, prefix)) .collect(); if f.cancellable { decls.push(format!("{prefix}_cancel_token* cancel_token = nullptr")); @@ -740,7 +743,7 @@ fn render_async_callable( /// values) are *owned by the consumer*: the wrapper copies or decodes them, /// then releases the producer allocation through the runtime free symbols. /// An owned interface result is instead adopted into the RAII wrapper. -fn emit_async_set_value(w: &mut CodeWriter, ty: &TypeRef, module: &str, prefix: &str) { +fn emit_async_set_value(w: &mut CodeWriter, ty: &Ty, module: &str, prefix: &str) { match ret_pass(Some(ty), module, prefix) { RetPass::Buffer => { // Owned `(result_ptr, result_len)` buffer: decode, then free. @@ -784,10 +787,10 @@ fn emit_async_set_value(w: &mut CodeWriter, ty: &TypeRef, module: &str, prefix: } } RetPass::Direct => match ty { - TypeRef::Handle => { + Ty::Handle => { w.line("p->set_value(reinterpret_cast(static_cast(result)));"); } - TypeRef::Enum(n) => { + Ty::Enum(n) => { w.line(format!( "p->set_value(static_cast<{}>(result));", local_type_name(n) @@ -828,10 +831,10 @@ pub(crate) fn render_cpp_module_ns(out: &mut String, module: &ModuleBinding, pre /// value types. Interface and typed-handle parameters stay raw borrowed /// pointers: wrapping a borrowed handle in the owning RAII class would /// `_destroy` it on destruction. -fn cpp_cb_param_type(ty: &TypeRef, module: &str, prefix: &str) -> String { +fn cpp_cb_param_type(ty: &Ty, module: &str, prefix: &str) -> String { match ty { - TypeRef::Interface(n) => format!("const {}*", c_abi_struct_name(n, module, prefix)), - TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Interface(_)) => { + Ty::Interface(n) => format!("const {}*", c_abi_struct_name(n, module, prefix)), + Ty::Optional(inner) if matches!(inner.as_ref(), Ty::Interface(_)) => { cpp_cb_param_type(inner, module, prefix) } other => cpp_type(other, module, prefix), @@ -875,11 +878,11 @@ fn emit_cb_arg(w: &mut CodeWriter, p: &ParamBinding, module: &str, prefix: &str) ArgPass::Direct { slot } => { let n0 = &slot.name; match &p.ty { - TypeRef::Enum(e) => format!( + Ty::Enum(e) => format!( "static_cast<{}>(static_cast({n0}))", local_type_name(e) ), - TypeRef::Handle => { + Ty::Handle => { format!("reinterpret_cast(static_cast({n0}))") } // Typed handles stay the raw borrowed tag pointer. diff --git a/crates/weaveffi-gen-cpp/src/codec.rs b/crates/weaveffi-gen-cpp/src/codec.rs index cfb9a003..a2f33c03 100644 --- a/crates/weaveffi-gen-cpp/src/codec.rs +++ b/crates/weaveffi-gen-cpp/src/codec.rs @@ -9,45 +9,39 @@ use weaveffi_core::codegen::common::DocCommentStyle; use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{EnumBinding, StructBinding}; +use weaveffi_core::model::{Prim, WireType}; use weaveffi_core::utils::local_type_name; -use weaveffi_core::wire::{self, WireType}; -use weaveffi_ir::ir::TypeRef; use crate::types::{cpp_ident, cpp_type}; /// Emit statements appending `expr` (a C++ lvalue of IDL type `ty`) to the /// buffer writer variable `wtr`, in wire order. `depth` disambiguates nested /// loop variable names. -pub(crate) fn emit_write_value( - w: &mut CodeWriter, - ty: &TypeRef, - expr: &str, - wtr: &str, - depth: usize, -) { +pub(crate) fn emit_write_value(w: &mut CodeWriter, ty: &Ty, expr: &str, wtr: &str, depth: usize) { let leaf = |w: &mut CodeWriter, method: &str| { w.line(format!("{wtr}.{method}({expr});")); }; - match wire::classify(ty) { - WireType::Bool => leaf(w, "write_bool"), - WireType::I8 => leaf(w, "write_i8"), - WireType::U8 => leaf(w, "write_u8"), - WireType::I16 => leaf(w, "write_i16"), - WireType::U16 => leaf(w, "write_u16"), - WireType::I32 => leaf(w, "write_i32"), - WireType::U32 => leaf(w, "write_u32"), - WireType::I64 => leaf(w, "write_i64"), - WireType::U64 => leaf(w, "write_u64"), - WireType::F32 => leaf(w, "write_f32"), - WireType::F64 => leaf(w, "write_f64"), - WireType::String => leaf(w, "write_string"), - WireType::Bytes => leaf(w, "write_bytes"), + match ty.wire() { + WireType::Prim(Prim::Bool) => leaf(w, "write_bool"), + WireType::Prim(Prim::I8) => leaf(w, "write_i8"), + WireType::Prim(Prim::U8) => leaf(w, "write_u8"), + WireType::Prim(Prim::I16) => leaf(w, "write_i16"), + WireType::Prim(Prim::U16) => leaf(w, "write_u16"), + WireType::Prim(Prim::I32) => leaf(w, "write_i32"), + WireType::Prim(Prim::U32) => leaf(w, "write_u32"), + WireType::Prim(Prim::I64) => leaf(w, "write_i64"), + WireType::Prim(Prim::U64) => leaf(w, "write_u64"), + WireType::Prim(Prim::F32) => leaf(w, "write_f32"), + WireType::Prim(Prim::F64) => leaf(w, "write_f64"), + WireType::Prim(Prim::String) => leaf(w, "write_string"), + WireType::Prim(Prim::Bytes) => leaf(w, "write_bytes"), WireType::Enum(_) => { w.line(format!("{wtr}.write_i32(static_cast({expr}));")); } // Handles are opaque tokens encoded as their pointer bits in a u64. - WireType::Handle => { + WireType::Handle(_) => { w.line(format!( "{wtr}.write_u64(static_cast(reinterpret_cast({expr})));" )); @@ -84,25 +78,25 @@ pub(crate) fn emit_write_value( /// The single expression decoding one leaf value from the reader variable /// `rdr`, or `None` when `ty` is a composite that needs statements. -fn read_leaf_expr(ty: &TypeRef, rdr: &str, module: &str, prefix: &str) -> Option { - Some(match wire::classify(ty) { - WireType::Bool => format!("{rdr}.read_bool()"), - WireType::I8 => format!("{rdr}.read_i8()"), - WireType::U8 => format!("{rdr}.read_u8()"), - WireType::I16 => format!("{rdr}.read_i16()"), - WireType::U16 => format!("{rdr}.read_u16()"), - WireType::I32 => format!("{rdr}.read_i32()"), - WireType::U32 => format!("{rdr}.read_u32()"), - WireType::I64 => format!("{rdr}.read_i64()"), - WireType::U64 => format!("{rdr}.read_u64()"), - WireType::F32 => format!("{rdr}.read_f32()"), - WireType::F64 => format!("{rdr}.read_f64()"), - WireType::String => format!("{rdr}.read_string()"), - WireType::Bytes => format!("{rdr}.read_bytes()"), +fn read_leaf_expr(ty: &Ty, rdr: &str, module: &str, prefix: &str) -> Option { + Some(match ty.wire() { + WireType::Prim(Prim::Bool) => format!("{rdr}.read_bool()"), + WireType::Prim(Prim::I8) => format!("{rdr}.read_i8()"), + WireType::Prim(Prim::U8) => format!("{rdr}.read_u8()"), + WireType::Prim(Prim::I16) => format!("{rdr}.read_i16()"), + WireType::Prim(Prim::U16) => format!("{rdr}.read_u16()"), + WireType::Prim(Prim::I32) => format!("{rdr}.read_i32()"), + WireType::Prim(Prim::U32) => format!("{rdr}.read_u32()"), + WireType::Prim(Prim::I64) => format!("{rdr}.read_i64()"), + WireType::Prim(Prim::U64) => format!("{rdr}.read_u64()"), + WireType::Prim(Prim::F32) => format!("{rdr}.read_f32()"), + WireType::Prim(Prim::F64) => format!("{rdr}.read_f64()"), + WireType::Prim(Prim::String) => format!("{rdr}.read_string()"), + WireType::Prim(Prim::Bytes) => format!("{rdr}.read_bytes()"), WireType::Enum(n) => format!("static_cast<{}>({rdr}.read_i32())", local_type_name(n)), // Both handle spellings decode from the same u64 token; the C++ // pointer type (`void*` or the prefixed tag) comes from the type map. - WireType::Handle => format!( + WireType::Handle(_) => format!( "reinterpret_cast<{}>(static_cast({rdr}.read_u64()))", cpp_type(ty, module, prefix) ), @@ -116,7 +110,7 @@ fn read_leaf_expr(ty: &TypeRef, rdr: &str, module: &str, prefix: &str) -> Option /// `tmp` seeds unique names for any temporaries the composite cases need. pub(crate) fn emit_read_into( w: &mut CodeWriter, - ty: &TypeRef, + ty: &Ty, target: &str, tmp: &str, rdr: &str, @@ -127,7 +121,7 @@ pub(crate) fn emit_read_into( w.line(format!("{target} = {expr};")); return; } - match wire::classify(ty) { + match ty.wire() { WireType::Optional(inner) => { w.line(format!("if ({rdr}.read_option_flag()) {{")); w.scope(|w| { @@ -183,7 +177,7 @@ pub(crate) fn emit_read_into( /// a single declaration; composites declare then fill. pub(crate) fn emit_read_decl( w: &mut CodeWriter, - ty: &TypeRef, + ty: &Ty, var: &str, rdr: &str, module: &str, diff --git a/crates/weaveffi-gen-cpp/src/entities.rs b/crates/weaveffi-gen-cpp/src/entities.rs index 9be0cbfa..e5ab0708 100644 --- a/crates/weaveffi-gen-cpp/src/entities.rs +++ b/crates/weaveffi-gen-cpp/src/entities.rs @@ -6,11 +6,11 @@ use std::collections::HashMap; use weaveffi_core::codegen::common::DocCommentStyle; use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ CallShape, EnumBinding, ErrorBinding, InterfaceBinding, ModuleBinding, StructBinding, }; use weaveffi_core::utils::local_type_name; -use weaveffi_ir::ir::TypeRef; use crate::calls::{render_cpp_callable, FnKind}; use crate::codec::emit_read_decl; @@ -87,11 +87,11 @@ impl ValueDef<'_> { /// Collect the local names of value types (records and rich enums) reachable /// from `ty`, recursing through optional/list/map wrappers. -fn collect_value_deps(ty: &TypeRef, deps: &mut Vec) { +fn collect_value_deps(ty: &Ty, deps: &mut Vec) { match ty { - TypeRef::Record(n) | TypeRef::RichEnum(n) => deps.push(local_type_name(n).to_string()), - TypeRef::Optional(inner) | TypeRef::List(inner) => collect_value_deps(inner, deps), - TypeRef::Map(k, v) => { + Ty::Record(n) | Ty::RichEnum(n) => deps.push(local_type_name(n).to_string()), + Ty::Optional(inner) | Ty::List(inner) => collect_value_deps(inner, deps), + Ty::Map(k, v) => { collect_value_deps(k, deps); collect_value_deps(v, deps); } @@ -103,10 +103,10 @@ fn collect_value_deps(ty: &TypeRef, deps: &mut Vec) { /// signatures (returned or accepted by value, so their classes must be /// complete first). pub(crate) fn interface_deps(i: &InterfaceBinding) -> Vec { - fn collect(ty: &TypeRef, deps: &mut Vec) { + fn collect(ty: &Ty, deps: &mut Vec) { match ty { - TypeRef::Interface(n) => deps.push(local_type_name(n).to_string()), - TypeRef::Optional(inner) | TypeRef::Iterator(inner) => collect(inner, deps), + Ty::Interface(n) => deps.push(local_type_name(n).to_string()), + Ty::Optional(inner) | Ty::Iterator(inner) => collect(inner, deps), _ => {} } } diff --git a/crates/weaveffi-gen-cpp/src/lib.rs b/crates/weaveffi-gen-cpp/src/lib.rs index 6f4e7a50..89ca3b68 100644 --- a/crates/weaveffi-gen-cpp/src/lib.rs +++ b/crates/weaveffi-gen-cpp/src/lib.rs @@ -46,23 +46,20 @@ mod codec; mod entities; mod package; mod runtime; -#[cfg(test)] -mod tests; mod types; use camino::Utf8Path; use serde::{Deserialize, Serialize}; -use weaveffi_core::abi::is_buffered; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::cabi; use weaveffi_core::capabilities::TargetCapabilities; +use weaveffi_core::model::Ty; use weaveffi_core::model::{BindingModel, CallShape, EnumBinding, InterfaceBinding, ModuleBinding}; use weaveffi_core::package::{PackageContext, PackagedFile}; use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::utils::{ render_abi_prefix_aliases, render_prelude, render_trailer, CommentStyle, }; -use weaveffi_ir::ir::TypeRef; use crate::calls::render_cpp_module_ns; use crate::entities::{ @@ -212,8 +209,6 @@ impl LanguageBackend for CppGenerator { } } -weaveffi_core::impl_generator_via_backend!(CppGenerator); - /// True when the API surface moves any value through the WeaveFFI buffer /// format, which requires emitting the private reader/writer runtime: any /// record or rich enum exists, any error code declares payload fields, or any @@ -227,12 +222,12 @@ fn model_needs_buffers(model: &BindingModel) -> bool { .is_some_and(|e| e.declared_here && e.codes.iter().any(|c| !c.fields.is_empty())) || m.callbacks .iter() - .any(|cb| cb.params.iter().any(|p| is_buffered(&p.ty))) + .any(|cb| cb.params.iter().any(|p| p.ty.is_buffered())) || m.callables().any(|f| { - f.params.iter().any(|p| is_buffered(&p.ty)) + f.params.iter().any(|p| p.ty.is_buffered()) || f.ret.as_ref().is_some_and(|r| match r { - TypeRef::Iterator(inner) => is_buffered(inner), - other => is_buffered(other), + Ty::Iterator(inner) => inner.is_buffered(), + other => other.is_buffered(), }) }) }) diff --git a/crates/weaveffi-gen-cpp/src/tests.rs b/crates/weaveffi-gen-cpp/src/tests.rs deleted file mode 100644 index 3ccba77d..00000000 --- a/crates/weaveffi-gen-cpp/src/tests.rs +++ /dev/null @@ -1,1802 +0,0 @@ -//! Tests for the C++ backend: rendered-header assertions over fixture APIs, -//! plus config, packaging, and determinism checks. - -use camino::Utf8Path; -use weaveffi_core::codegen::Generator; -use weaveffi_core::lang::{self, CPP_KEYWORDS}; -use weaveffi_core::model::BindingModel; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_ir::ir::{ - Api, CallbackDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, InterfaceDef, - ListenerDef, Module, Param, StructDef, StructField, TypeRef, -}; - -use crate::render_cpp_header; -use crate::types::{cpp_fn_name, cpp_ident, cpp_type, CPP_EXTRA_KEYWORDS}; -use crate::{CppConfig, CppGenerator}; - -fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - mutable: false, - doc: None, - } -} - -fn field(name: &str, ty: TypeRef) -> StructField { - StructField { - name: name.into(), - ty, - doc: None, - } -} - -fn variant(name: &str, value: i32, fields: Vec) -> EnumVariant { - EnumVariant { - name: name.into(), - value, - doc: None, - fields, - } -} - -fn code(name: &str, value: i32, message: &str) -> ErrorCode { - ErrorCode { - name: name.into(), - code: value, - message: message.into(), - doc: None, - fields: vec![], - } -} - -/// A plain sync, non-throwing function. -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, - since: None, - } -} - -/// A sync function that throws its module's error domain. -fn tfunc(name: &str, params: Vec, returns: Option) -> Function { - Function { - throws: true, - ..func(name, params, returns) - } -} - -fn empty_module(name: &str) -> Module { - Module { - name: name.into(), - functions: vec![], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - } -} - -fn api_of(modules: Vec) -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules, - generators: None, - package: None, - }) -} - -/// Render with the default namespace and prefix, as the driver would. -fn render(api: &ResolvedApi) -> String { - let model = BindingModel::build(api, "weaveffi"); - render_cpp_header(&model, "weaveffi", "weaveffi.yml", "weaveffi.hpp") -} - -fn minimal_api() -> ResolvedApi { - let mut m = empty_module("calculator"); - m.functions = vec![func( - "add", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - )]; - api_of(vec![m]) -} - -fn contacts_api() -> ResolvedApi { - let mut m = empty_module("contacts"); - m.enums = vec![EnumDef { - name: "ContactType".into(), - doc: None, - variants: vec![variant("Personal", 0, vec![]), variant("Work", 1, vec![])], - }]; - m.structs = vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - field("name", TypeRef::StringUtf8), - field("age", TypeRef::I32), - field("email", TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - field("contact_type", TypeRef::Enum("ContactType".into())), - ], - }]; - m.functions = vec![ - func( - "get_contact", - vec![param("id", TypeRef::Handle)], - Some(TypeRef::Record("Contact".into())), - ), - func("delete_contact", vec![param("id", TypeRef::Handle)], None), - func( - "save_contact", - vec![param("contact", TypeRef::Record("Contact".into()))], - Some(TypeRef::Bool), - ), - ]; - api_of(vec![m]) -} - -/// A kvstore-shaped fixture: error domain (one code with payload fields), -/// enum, struct, an interface with a factory constructor, -/// sync/iterator/async methods, a static, and a nested module whose -/// function takes the interface across modules. -fn kvstore_api() -> ResolvedApi { - let mut kv = empty_module("kv"); - kv.errors = Some(ErrorDomain { - name: "KvError".into(), - codes: vec![ - ErrorCode { - fields: vec![field("key", TypeRef::StringUtf8)], - ..code("KeyNotFound", 1001, "key not found") - }, - code("IoError", 1004, "I/O failure"), - ], - }); - kv.enums = vec![EnumDef { - name: "EntryKind".into(), - doc: None, - variants: vec![ - variant("Volatile", 0, vec![]), - variant("Persistent", 1, vec![]), - ], - }]; - kv.structs = vec![StructDef { - name: "Entry".into(), - doc: None, - fields: vec![field("key", TypeRef::StringUtf8)], - }]; - kv.interfaces = vec![InterfaceDef { - name: "Store".into(), - doc: Some("An embedded key-value store owning its entries".into()), - constructors: vec![tfunc( - "open", - vec![param("path", TypeRef::StringUtf8)], - None, - )], - methods: vec![ - tfunc( - "put", - vec![ - param("key", TypeRef::StringUtf8), - param("value", TypeRef::Bytes), - param("kind", TypeRef::Enum("EntryKind".into())), - param("ttl_seconds", TypeRef::Optional(Box::new(TypeRef::I64))), - ], - Some(TypeRef::Bool), - ), - tfunc( - "get", - vec![param("key", TypeRef::StringUtf8)], - Some(TypeRef::Optional(Box::new(TypeRef::Record("Entry".into())))), - ), - tfunc( - "delete", - vec![param("key", TypeRef::StringUtf8)], - Some(TypeRef::Bool), - ), - tfunc( - "list_keys", - vec![param( - "prefix", - TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - )], - Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - ), - func("count", vec![], Some(TypeRef::I64)), - Function { - r#async: true, - cancellable: true, - ..tfunc("compact", vec![], Some(TypeRef::I64)) - }, - Function { - deprecated: Some("use put() with explicit kind".into()), - ..tfunc( - "legacy_put", - vec![param("key", TypeRef::StringUtf8)], - Some(TypeRef::Bool), - ) - }, - ], - statics: vec![func("default_capacity", vec![], Some(TypeRef::I64))], - }]; - kv.callbacks = vec![CallbackDef { - name: "OnEvict".into(), - doc: None, - params: vec![param("key", TypeRef::StringUtf8)], - }]; - kv.listeners = vec![ListenerDef { - name: "eviction_listener".into(), - event_callback: "OnEvict".into(), - doc: None, - }]; - - let mut stats = empty_module("stats"); - stats.structs = vec![StructDef { - name: "Stats".into(), - doc: None, - fields: vec![field("total_entries", TypeRef::I64)], - }]; - stats.functions = vec![tfunc( - "get_stats", - vec![param("store", TypeRef::Interface("kv.Store".into()))], - Some(TypeRef::Record("Stats".into())), - )]; - kv.modules = vec![stats]; - api_of(vec![kv]) -} - -#[test] -fn extra_keyword_table_sorted_and_disjoint_from_shared() { - assert!( - CPP_EXTRA_KEYWORDS.windows(2).all(|w| w[0] < w[1]), - "extra keyword table must be sorted and duplicate-free" - ); - for kw in CPP_EXTRA_KEYWORDS { - assert!( - !lang::is_reserved(kw, CPP_KEYWORDS), - "'{kw}' is already in the shared table and must not be duplicated" - ); - } -} - -#[test] -fn cpp_ident_escapes_keywords() { - assert_eq!(cpp_ident("delete"), "delete_"); - assert_eq!(cpp_ident("new"), "new_"); - assert_eq!(cpp_ident("key"), "key"); - assert_eq!(cpp_fn_name("listKeys"), "list_keys"); - assert_eq!(cpp_fn_name("delete"), "delete_"); -} - -/// Regression: reserved words missing from the shared table (alternative -/// operator tokens, extended character types, cast keywords, and -/// `thread_local`) must keep escaping exactly as before the refactor onto -/// `weaveffi_core::lang`. -#[test] -fn cpp_ident_escapes_extended_keywords() { - assert_eq!(cpp_ident("thread_local"), "thread_local_"); - assert_eq!(cpp_ident("wchar_t"), "wchar_t_"); - assert_eq!(cpp_ident("char8_t"), "char8_t_"); - assert_eq!(cpp_ident("const_cast"), "const_cast_"); - assert_eq!(cpp_ident("reinterpret_cast"), "reinterpret_cast_"); - assert_eq!(cpp_ident("xor_eq"), "xor_eq_"); - // And a shared-table spot check right beside them. - assert_eq!(cpp_ident("co_await"), "co_await_"); - assert_eq!(cpp_ident("static_cast"), "static_cast_"); -} - -#[test] -fn package_bundles_header_libs_and_cmake() { - use camino::Utf8Path; - use weaveffi_core::backend::LanguageBackend; - use weaveffi_core::package::{FileContent, PackageContext}; - use weaveffi_core::platform::{BinarySet, Platform}; - - let api = api_of(vec![empty_module("calc")]); - let model = BindingModel::build(&api, "weaveffi"); - let mut bins = BinarySet::new("calculator"); - bins.insert(Platform::MacosArm64, "/s/darwin-arm64/libcalculator.dylib"); - bins.insert(Platform::LinuxX64, "/s/linux-x64/libcalculator.so"); - let ctx = PackageContext { - binaries: &bins, - input_basename: Some("calculator.yml"), - }; - let files = LanguageBackend::package( - &CppGenerator, - &api, - &model, - &ctx, - Utf8Path::new("/out"), - &CppConfig::default(), - ) - .expect("cpp supports packaging"); - - assert_eq!(files.iter().filter(|f| f.is_binary()).count(), 2); - assert!(files - .iter() - .any(|f| f.path.as_str().ends_with("cpp/include/weaveffi.hpp"))); - assert!(files.iter().any(|f| f - .path - .as_str() - .ends_with("cpp/lib/linux-x64/libcalculator.so"))); - let cmake = files - .iter() - .find(|f| f.path.as_str().ends_with("cpp/CMakeLists.txt")) - .expect("CMakeLists present"); - let FileContent::Text(txt) = &cmake.content else { - panic!("CMakeLists is text"); - }; - assert!( - txt.contains("IMPORTED") - && txt.contains("libcalculator.dylib") - && txt.contains("weaveffi_cpp"), - "imported target missing: {txt}" - ); -} - -#[test] -fn listeners_generate_register_unregister() { - let mut m = empty_module("events"); - m.callbacks = vec![CallbackDef { - name: "OnMessage".into(), - doc: None, - params: vec![param("message", TypeRef::StringUtf8)], - }]; - m.listeners = vec![ListenerDef { - name: "message_listener".into(), - event_callback: "OnMessage".into(), - doc: None, - }]; - let hpp = render(&api_of(vec![m])); - assert!( - hpp.contains("#include ") && hpp.contains("#include "), - "listener includes missing: {hpp}" - ); - assert!( - hpp.contains("namespace events {"), - "listener should live in the module namespace: {hpp}" - ); - assert!( - hpp.contains( - "inline uint64_t register_message_listener(std::function callback)" - ), - "register wrapper missing: {hpp}" - ); - assert!( - hpp.contains("inline void unregister_message_listener(uint64_t id)"), - "unregister wrapper missing: {hpp}" - ); - assert!( - hpp.contains("detail::wv_listener_registry()[id] = fn;"), - "closure box must be pinned in the registry: {hpp}" - ); - assert!( - hpp.contains("cb(std::string(message ? message : \"\"));"), - "trampoline must convert the string arg: {hpp}" - ); - assert!( - hpp.contains("detail::wv_listener_registry().erase(id);"), - "unregister must drop the box: {hpp}" - ); -} - -/// A listener whose callback carries a buffered argument decodes the -/// borrowed `(ptr, len)` pair before invoking the user's `std::function`. -#[test] -fn listener_buffered_argument_is_decoded_before_dispatch() { - let mut m = empty_module("events"); - m.structs = vec![StructDef { - name: "Event".into(), - doc: None, - fields: vec![field("id", TypeRef::I64)], - }]; - m.callbacks = vec![CallbackDef { - name: "OnEvent".into(), - doc: None, - params: vec![param("event", TypeRef::Record("Event".into()))], - }]; - m.listeners = vec![ListenerDef { - name: "events".into(), - event_callback: "OnEvent".into(), - doc: None, - }]; - let h = render(&api_of(vec![m])); - // The user callback receives the decoded value type. - assert!( - h.contains("inline uint64_t register_events(std::function callback)"), - "callback should surface the decoded value type: {h}" - ); - // Trampoline slots are the borrowed pair. - assert!( - h.contains("[](const uint8_t* event_ptr, size_t event_len, void* context)"), - "trampoline should take the borrowed buffer slots: {h}" - ); - assert!( - h.contains("detail::BufferReader event_r(event_ptr, event_len);"), - "trampoline should decode the borrowed buffer: {h}" - ); - assert!( - h.contains("cb(std::move(event_val));"), - "decoded value should be handed to the user callback: {h}" - ); - assert!( - !h.contains("weaveffi_free_bytes(const_cast(event_ptr)"), - "borrowed callback buffers must never be freed: {h}" - ); -} - -#[test] -fn name_returns_cpp() { - assert_eq!(Generator::name(&CppGenerator), "cpp"); -} - -#[test] -fn output_files_lists_hpp() { - let api = minimal_api(); - let out_dir = Utf8Path::new("/tmp/out"); - let files = CppGenerator.output_files(&api, out_dir, &CppConfig::default()); - assert_eq!( - files, - vec![ - format!("{out_dir}/cpp/CMakeLists.txt"), - format!("{out_dir}/cpp/README.md"), - format!("{out_dir}/cpp/weaveffi.hpp"), - ] - ); -} - -#[test] -fn generate_creates_hpp_file() { - let api = minimal_api(); - let tmp = std::env::temp_dir().join("weaveffi_test_cpp_gen"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8"); - - CppGenerator - .generate(&api, out_dir, &CppConfig::default()) - .unwrap(); - - let hpp = tmp.join("cpp").join("weaveffi.hpp"); - assert!(hpp.exists(), "weaveffi.hpp should be created"); - - let content = std::fs::read_to_string(&hpp).unwrap(); - assert!(content.contains("#pragma once"), "missing pragma once"); - assert!( - content.contains("#include "), - "missing cstdint include" - ); - assert!(content.contains("extern \"C\""), "missing extern C block"); - assert!(content.contains("namespace weaveffi"), "missing namespace"); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn cpp_generates_cmake() { - let api = minimal_api(); - let tmp = std::env::temp_dir().join("weaveffi_test_cpp_cmake"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8"); - - CppGenerator - .generate(&api, out_dir, &CppConfig::default()) - .unwrap(); - - let cmake = tmp.join("cpp").join("CMakeLists.txt"); - assert!(cmake.exists(), "CMakeLists.txt should be created"); - - let content = std::fs::read_to_string(&cmake).unwrap(); - assert!( - content.contains("cmake_minimum_required"), - "missing cmake_minimum_required" - ); - assert!( - content.contains("project(weaveffi_cpp VERSION 0.1.0)"), - "missing project declaration with version" - ); - assert!( - content.contains("add_library(weaveffi_cpp INTERFACE)"), - "missing interface library" - ); - assert!( - content.contains("target_compile_features(weaveffi_cpp INTERFACE cxx_std_17)"), - "missing C++17 requirement" - ); - - let readme = tmp.join("cpp").join("README.md"); - assert!(readme.exists(), "README.md should be created"); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn header_includes() { - let h = render(&minimal_api()); - for inc in [ - "", - "", - "", - "", - "", - "", - "", - "", - ] { - assert!( - h.contains(&format!("#include {inc}")), - "missing include {inc}" - ); - } -} - -/// The buffer runtime (and its ``/`` includes) is -/// emitted only when some value actually crosses the ABI in a buffer. -#[test] -fn buffer_runtime_emitted_only_when_needed() { - let plain = render(&minimal_api()); - assert!( - !plain.contains("class BufferWriter") && !plain.contains("#include "), - "a buffer-free API must not carry the buffer runtime: {plain}" - ); - let buffered = render(&contacts_api()); - for needle in [ - "#include ", - "#include ", - "class BufferWriter", - "class BufferReader", - "struct BufferGuard", - ] { - assert!( - buffered.contains(needle), - "missing buffer runtime piece {needle}: {buffered}" - ); - } -} - -#[test] -fn extern_c_common_declarations() { - let h = render(&minimal_api()); - assert!( - h.contains("typedef uint64_t weaveffi_handle_t;"), - "missing handle_t typedef" - ); - assert!( - h.contains("typedef struct weaveffi_error"), - "missing error struct" - ); - assert!( - h.contains("const uint8_t* payload_ptr;") && h.contains("size_t payload_len;"), - "error struct must carry the payload slots: {h}" - ); - assert!( - h.contains("void weaveffi_error_clear(weaveffi_error* err);"), - "missing error_clear" - ); - assert!( - h.contains("void weaveffi_free_string(const char* ptr);"), - "missing free_string" - ); - assert!( - h.contains("void weaveffi_free_bytes(uint8_t* ptr, size_t len);"), - "missing free_bytes" - ); -} - -#[test] -fn visibility_macro_defined_and_applied() { - let h = render(&minimal_api()); - assert!(h.contains("#ifndef WEAVEFFI_API"), "missing macro guard"); - assert!( - h.contains("# define WEAVEFFI_API __attribute__((visibility(\"default\")))"), - "missing GCC/Clang visibility branch" - ); - assert!( - h.contains("WEAVEFFI_API void weaveffi_free_string(const char* ptr);"), - "runtime helper not tagged for export" - ); -} - -#[test] -fn extern_c_function_declarations() { - let h = render(&minimal_api()); - assert!( - h.contains( - "int32_t weaveffi_calculator_add(int32_t a, int32_t b, weaveffi_error* out_err);" - ), - "missing add declaration: {h}" - ); -} - -#[test] -fn extern_c_enum_declarations() { - let h = render(&contacts_api()); - assert!( - h.contains("weaveffi_contacts_ContactType_Personal = 0"), - "missing enum variant: {h}" - ); - assert!( - h.contains("weaveffi_contacts_ContactType_Work = 1"), - "missing enum variant: {h}" - ); - assert!( - h.contains("} weaveffi_contacts_ContactType;"), - "missing enum typedef: {h}" - ); -} - -/// Records are value types: the extern C block declares no create, -/// destroy, getter, or tag symbols for them, and buffered functions take -/// and return `(const uint8_t*, size_t)` pairs. -#[test] -fn extern_c_records_have_no_symbols() { - let h = render(&contacts_api()); - assert!( - !h.contains("weaveffi_contacts_Contact_create") - && !h.contains("weaveffi_contacts_Contact_destroy") - && !h.contains("weaveffi_contacts_Contact_get_"), - "records must have no C symbols: {h}" - ); - assert!( - !h.contains("typedef struct weaveffi_contacts_Contact"), - "records must not declare an opaque tag: {h}" - ); - assert!( - h.contains( - "const uint8_t* weaveffi_contacts_get_contact(weaveffi_handle_t id, size_t* out_len, weaveffi_error* out_err);" - ), - "buffered return should use the bytes shape: {h}" - ); - assert!( - h.contains( - "bool weaveffi_contacts_save_contact(const uint8_t* contact_ptr, size_t contact_len, weaveffi_error* out_err);" - ), - "buffered param should expand to ptr+len slots: {h}" - ); -} - -#[test] -fn cpp_enum_class() { - let h = render(&contacts_api()); - assert!( - h.contains("enum class ContactType : int32_t {"), - "missing enum class: {h}" - ); - assert!(h.contains("Personal = 0,"), "missing Personal variant: {h}"); - assert!(h.contains("Work = 1"), "missing Work variant: {h}"); -} - -/// A record renders as a plain value struct: typed members in wire order, -/// no handle, no destructor, no getters, no builders. -#[test] -fn cpp_record_is_a_value_struct() { - let h = render(&contacts_api()); - assert!(h.contains("struct Contact {"), "missing value struct: {h}"); - assert!( - h.contains("std::string name;") - && h.contains("int32_t age;") - && h.contains("std::optional email;") - && h.contains("ContactType contact_type;"), - "missing typed members: {h}" - ); - assert!( - !h.contains("class Contact {") - && !h.contains("~Contact()") - && !h.contains("ContactBuilder"), - "records must not be RAII classes or have builders: {h}" - ); -} - -/// Each record gets one pack and one unpack routine in `detail`, -/// serializing fields in declaration order per the wire format. -#[test] -fn cpp_record_codec_round_trip_shape() { - let h = render(&contacts_api()); - let wf = &h[h - .find("inline void write_Contact(BufferWriter& w, const Contact& v) {") - .expect("write codec")..]; - let wf = &wf[..wf.find("\n}\n").unwrap()]; - assert!( - wf.contains("w.write_string(v.name);") - && wf.contains("w.write_i32(v.age);") - && wf.contains("w.write_option_flag(v.email.has_value());") - && wf.contains("w.write_string((*v.email));") - && wf.contains("w.write_i32(static_cast(v.contact_type));"), - "write codec must serialize fields in order: {wf}" - ); - let rf = &h[h - .find("inline Contact read_Contact(BufferReader& r) {") - .expect("read codec")..]; - let rf = &rf[..rf.find("\n}\n").unwrap()]; - assert!( - rf.contains("out.name = r.read_string();") - && rf.contains("out.age = r.read_i32();") - && rf.contains("if (r.read_option_flag()) {") - && rf.contains("out.contact_type = static_cast(r.read_i32());"), - "read codec must decode fields in order: {rf}" - ); -} - -#[test] -fn cpp_wrapper_function_scalar() { - let h = render(&minimal_api()); - assert!( - h.contains("inline int32_t add(int32_t a, int32_t b) {"), - "missing bare-named wrapper function: {h}" - ); - assert!( - h.contains("weaveffi_calculator_add(a, b, &err)"), - "should call C function: {h}" - ); - assert!( - h.contains("detail::check(err);"), - "non-throwing wrapper should use the generic check: {h}" - ); - assert!(h.contains("return result;"), "should return result: {h}"); -} - -#[test] -fn cpp_functions_live_in_module_namespace() { - let h = render(&minimal_api()); - let ns_open = h.find("namespace calculator {").expect("module namespace"); - let ns_close = h - .find("} // namespace calculator") - .expect("module namespace close"); - let fn_pos = h.find("inline int32_t add").expect("wrapper"); - assert!( - fn_pos > ns_open && fn_pos < ns_close, - "function should be inside the module namespace" - ); - let outer_open = h.find("namespace weaveffi {").unwrap(); - let outer_close = h.find("} // namespace weaveffi").unwrap(); - assert!( - ns_open > outer_open && ns_close < outer_close, - "module namespace should nest inside the configured namespace" - ); - assert!( - !h.contains("inline int32_t calculator_add("), - "module-prefixed wrapper names must be gone: {h}" - ); -} - -#[test] -fn cpp_nested_module_namespace_path() { - let h = render(&kvstore_api()); - assert!( - h.contains("namespace kv::stats {"), - "nested module should use a nested namespace: {h}" - ); - assert!( - h.contains("inline Stats get_stats(const Store& store)"), - "nested function should be bare-named and borrow the interface: {h}" - ); - assert!( - h.contains("static_cast(store.handle())"), - "interface param should pass the borrowed handle: {h}" - ); -} - -/// A record return decodes the producer buffer and releases it through -/// the scope guard. -#[test] -fn cpp_wrapper_function_record_return_decodes_buffer() { - let h = render(&contacts_api()); - assert!( - h.contains("inline Contact get_contact(void* id) {"), - "missing record-returning function: {h}" - ); - let f = &h[h.find("inline Contact get_contact").unwrap()..]; - let f = &f[..f.find("\n}\n").unwrap()]; - assert!( - f.contains("size_t out_len = 0;"), - "buffered return needs out_len: {f}" - ); - assert!( - f.contains("detail::BufferGuard result_guard{result, out_len};"), - "producer buffer must be released via the guard: {f}" - ); - assert!( - f.contains("detail::BufferReader result_r(result, out_len);") - && f.contains("Contact ret = detail::read_Contact(result_r);") - && f.contains("result_r.expect_end();") - && f.contains("return ret;"), - "buffered return must decode through the codec: {f}" - ); -} - -/// A record parameter packs into a local buffer and passes -/// `(data(), size())`; the caller keeps ownership of the value. -#[test] -fn cpp_wrapper_function_record_param_packs_buffer() { - let h = render(&contacts_api()); - assert!( - h.contains("inline bool save_contact(const Contact& contact) {"), - "record param should borrow by const ref: {h}" - ); - let f = &h[h.find("inline bool save_contact").unwrap()..]; - let f = &f[..f.find("\n}\n").unwrap()]; - assert!( - f.contains("detail::BufferWriter contact_buf;") - && f.contains("detail::write_Contact(contact_buf, contact);"), - "record param must pack through the codec: {f}" - ); - assert!( - f.contains("weaveffi_contacts_save_contact(contact_buf.data(), contact_buf.size(), &err)"), - "packed buffer should pass as ptr+len: {f}" - ); -} - -#[test] -fn cpp_wrapper_function_void_return() { - let h = render(&contacts_api()); - assert!( - h.contains("inline void delete_contact(void* id) {"), - "missing void function: {h}" - ); - let void_fn_start = h.find("inline void delete_contact").unwrap(); - let void_fn = &h[void_fn_start..(void_fn_start + 300).min(h.len())]; - assert!( - !void_fn.contains("return result"), - "void function should not return a value: {void_fn}" - ); -} - -#[test] -fn cpp_wrapper_handle_param_conversion() { - let h = render(&contacts_api()); - assert!( - h.contains("static_cast(reinterpret_cast(id))"), - "should convert void* to handle_t: {h}" - ); -} - -#[test] -fn cpp_wrapper_error_handling() { - let h = render(&minimal_api()); - assert!( - h.contains("weaveffi_error err{};"), - "should declare error: {h}" - ); - assert!( - h.contains("if (err.code == 0) return;"), - "check helper should early-return on success: {h}" - ); - assert!( - h.contains("weaveffi_error_clear(&err)"), - "should clear error: {h}" - ); - assert!( - h.contains("throw WeaveFFIError(code, msg);"), - "generic check should throw the brand error: {h}" - ); -} - -#[test] -fn cpp_string_param_function() { - let mut m = empty_module("io"); - m.functions = vec![func( - "echo", - vec![param("msg", TypeRef::StringUtf8)], - Some(TypeRef::StringUtf8), - )]; - let h = render(&api_of(vec![m])); - assert!( - h.contains("inline std::string echo(const std::string& msg)"), - "string param should be const ref: {h}" - ); - assert!(h.contains("msg.c_str()"), "should pass c_str: {h}"); - assert!( - h.contains("weaveffi_free_string(result)"), - "should free returned string: {h}" - ); -} - -/// A list return is one value buffer: decoded elementwise, then the -/// producer buffer is released through the guard. -#[test] -fn cpp_list_return_function() { - let mut m = empty_module("store"); - m.functions = vec![func( - "list_ids", - vec![], - Some(TypeRef::List(Box::new(TypeRef::I32))), - )]; - let h = render(&api_of(vec![m])); - assert!( - h.contains("inline std::vector list_ids()"), - "missing list return function: {h}" - ); - let f = &h[h.find("inline std::vector list_ids()").unwrap()..]; - let f = &f[..f.find("\n}\n").unwrap()]; - assert!( - f.contains("size_t out_len = 0;"), - "should declare out_len: {f}" - ); - assert!( - f.contains("detail::BufferGuard result_guard{result, out_len};"), - "list buffer must be released via the guard: {f}" - ); - assert!( - f.contains("size_t ret_n = result_r.read_len();") - && f.contains("ret.reserve(ret_n);") - && f.contains("int32_t ret_item = result_r.read_i32();") - && f.contains("ret.push_back(std::move(ret_item));"), - "list return must decode elementwise: {f}" - ); -} - -/// An optional scalar return decodes the presence flag from the buffer. -#[test] -fn cpp_optional_i32_return() { - let mut m = empty_module("store"); - m.functions = vec![func( - "find", - vec![param("id", TypeRef::I32)], - Some(TypeRef::Optional(Box::new(TypeRef::I32))), - )]; - let h = render(&api_of(vec![m])); - assert!( - h.contains("inline std::optional find(int32_t id)"), - "missing optional return function: {h}" - ); - let f = &h[h.find("inline std::optional find").unwrap()..]; - let f = &f[..f.find("\n}\n").unwrap()]; - assert!( - f.contains("std::optional ret{};") - && f.contains("if (result_r.read_option_flag()) {") - && f.contains("int32_t ret_v = result_r.read_i32();") - && f.contains("ret = std::move(ret_v);"), - "optional return must decode the flag byte then the value: {f}" - ); -} - -#[test] -fn cpp_enum_param_function() { - let mut m = empty_module("paint"); - m.enums = vec![EnumDef { - name: "Color".into(), - doc: None, - variants: vec![variant("Red", 0, vec![]), variant("Green", 1, vec![])], - }]; - m.functions = vec![func( - "mix", - vec![param("color", TypeRef::Enum("Color".into()))], - Some(TypeRef::Enum("Color".into())), - )]; - let h = render(&api_of(vec![m])); - assert!( - h.contains("inline Color mix(Color color)"), - "missing enum function: {h}" - ); - assert!( - h.contains("static_cast(static_cast(color))"), - "should double-cast enum param: {h}" - ); - assert!( - h.contains("return static_cast(result);"), - "should cast return to enum class: {h}" - ); -} - -/// A list of records decodes each element through the record codec. -#[test] -fn cpp_list_record_return() { - let mut m = empty_module("contacts"); - m.structs = vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }]; - m.functions = vec![func( - "list_all", - vec![], - Some(TypeRef::List(Box::new(TypeRef::Record("Contact".into())))), - )]; - let h = render(&api_of(vec![m])); - assert!( - h.contains("inline std::vector list_all()"), - "missing list record return: {h}" - ); - let f = &h[h.find("inline std::vector list_all()").unwrap()..]; - let f = &f[..f.find("\n}\n").unwrap()]; - assert!( - f.contains("Contact ret_item = detail::read_Contact(result_r);") - && f.contains("ret.push_back(std::move(ret_item));"), - "each element must decode through the codec: {f}" - ); -} - -/// A map return is one value buffer of alternating key, value entries. -#[test] -fn cpp_map_return_function() { - let mut m = empty_module("store"); - m.functions = vec![func( - "get_scores", - vec![], - Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )), - )]; - let h = render(&api_of(vec![m])); - assert!( - h.contains("inline std::unordered_map get_scores()"), - "missing map return function: {h}" - ); - let f = &h[h - .find("inline std::unordered_map get_scores()") - .unwrap()..]; - let f = &f[..f.find("\n}\n").unwrap()]; - assert!( - f.contains("std::string ret_key = result_r.read_string();") - && f.contains("int32_t ret_val = result_r.read_i32();") - && f.contains("ret.emplace(std::move(ret_key), std::move(ret_val));"), - "map decode must alternate key then value: {f}" - ); - assert!( - f.contains("detail::BufferGuard result_guard{result, out_len};"), - "map buffer must be released via the guard: {f}" - ); -} - -/// A list parameter packs its length prefix then each element. -#[test] -fn cpp_list_param_packs_buffer() { - let mut m = empty_module("data"); - m.functions = vec![func( - "sum", - vec![param("ids", TypeRef::List(Box::new(TypeRef::I32)))], - Some(TypeRef::I64), - )]; - let h = render(&api_of(vec![m])); - assert!( - h.contains("inline int64_t sum(const std::vector& ids)"), - "list param should borrow by const ref: {h}" - ); - let f = &h[h.find("inline int64_t sum").unwrap()..]; - let f = &f[..f.find("\n}\n").unwrap()]; - assert!( - f.contains("ids_buf.write_len(ids.size());") - && f.contains("for (const auto& item0 : ids) {") - && f.contains("ids_buf.write_i32(item0);"), - "list param must pack a count then each element: {f}" - ); - assert!( - f.contains("weaveffi_data_sum(ids_buf.data(), ids_buf.size(), &err)"), - "packed list should pass as ptr+len: {f}" - ); -} - -#[test] -fn cpp_type_mapping() { - assert_eq!(cpp_type(&TypeRef::I32, "m", "weaveffi"), "int32_t"); - assert_eq!(cpp_type(&TypeRef::U32, "m", "weaveffi"), "uint32_t"); - assert_eq!(cpp_type(&TypeRef::I64, "m", "weaveffi"), "int64_t"); - assert_eq!(cpp_type(&TypeRef::F64, "m", "weaveffi"), "double"); - assert_eq!(cpp_type(&TypeRef::Bool, "m", "weaveffi"), "bool"); - assert_eq!( - cpp_type(&TypeRef::StringUtf8, "m", "weaveffi"), - "std::string" - ); - assert_eq!( - cpp_type(&TypeRef::Bytes, "m", "weaveffi"), - "std::vector" - ); - assert_eq!(cpp_type(&TypeRef::Handle, "m", "weaveffi"), "void*"); - assert_eq!( - cpp_type(&TypeRef::TypedHandle("Session".into()), "db", "weaveffi"), - "weaveffi_db_Session*" - ); - assert_eq!( - cpp_type( - &TypeRef::TypedHandle("auth.Session".into()), - "db", - "weaveffi" - ), - "weaveffi_auth_Session*" - ); - assert_eq!( - cpp_type(&TypeRef::Record("Contact".into()), "m", "weaveffi"), - "Contact" - ); - assert_eq!( - cpp_type(&TypeRef::RichEnum("Shape".into()), "m", "weaveffi"), - "Shape" - ); - assert_eq!( - cpp_type(&TypeRef::RichEnum("geo.Shape".into()), "m", "weaveffi"), - "Shape" - ); - assert_eq!( - cpp_type(&TypeRef::Enum("Color".into()), "m", "weaveffi"), - "Color" - ); - assert_eq!( - cpp_type(&TypeRef::Interface("Store".into()), "m", "weaveffi"), - "Store" - ); - assert_eq!( - cpp_type(&TypeRef::Interface("kv.Store".into()), "m", "weaveffi"), - "Store" - ); - assert_eq!( - cpp_type(&TypeRef::Optional(Box::new(TypeRef::I32)), "m", "weaveffi"), - "std::optional" - ); - assert_eq!( - cpp_type(&TypeRef::List(Box::new(TypeRef::I32)), "m", "weaveffi"), - "std::vector" - ); - assert_eq!( - cpp_type( - &TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - "m", - "weaveffi" - ), - "std::unordered_map" - ); -} - -#[test] -fn cpp_extern_c_wrapping() { - let h = render(&minimal_api()); - let ext_open = h.find("extern \"C\" {").unwrap(); - let ext_close = h.find("} // extern \"C\"").unwrap(); - let c_fn = h.find("weaveffi_calculator_add(").unwrap(); - assert!( - c_fn > ext_open && c_fn < ext_close, - "C declarations should be inside extern C" - ); -} - -#[test] -fn cpp_bytes_return_function() { - let mut m = empty_module("io"); - m.functions = vec![func("read", vec![], Some(TypeRef::Bytes))]; - let h = render(&api_of(vec![m])); - assert!( - h.contains("inline std::vector read()"), - "missing bytes return function: {h}" - ); - assert!(h.contains("weaveffi_free_bytes("), "should free bytes: {h}"); -} - -/// A typed handle is an opaque token: it surfaces as the raw prefixed tag -/// pointer and passes straight through. -#[test] -fn cpp_typed_handle_param() { - let mut m = empty_module("db"); - m.structs = vec![StructDef { - name: "Connection".into(), - doc: None, - fields: vec![], - }]; - m.functions = vec![func( - "query", - vec![param("conn", TypeRef::TypedHandle("Connection".into()))], - Some(TypeRef::I32), - )]; - let h = render(&api_of(vec![m])); - assert!( - h.contains("inline int32_t query(weaveffi_db_Connection* conn)"), - "typed handle param should be the raw tag pointer: {h}" - ); - assert!( - h.contains("weaveffi_db_query(conn, &err)"), - "typed handle should pass through unchanged: {h}" - ); -} - -#[test] -fn cpp_has_error_class() { - let h = render(&minimal_api()); - assert!( - h.contains("class WeaveFFIError : public std::runtime_error"), - "missing WeaveFFIError class: {h}" - ); - assert!(h.contains("int32_t code_"), "missing code_ member: {h}"); - assert!( - h.contains("WeaveFFIError(int32_t code, const std::string& msg) : std::runtime_error(msg), code_(code) {}"), - "missing constructor: {h}" - ); - assert!( - h.contains("int32_t code() const { return code_; }"), - "missing code() getter: {h}" - ); -} - -// ── Interface (RAII) tests ── - -#[test] -fn interface_generates_raii_class() { - let h = render(&kvstore_api()); - assert!(h.contains("class Store {"), "missing Store class: {h}"); - assert!( - h.contains("~Store() {") - && h.contains( - "if (handle_) weaveffi_kv_Store_destroy(static_cast(handle_));" - ), - "destructor should call C destroy: {h}" - ); - assert!( - h.contains("Store(const Store&) = delete;"), - "copy constructor should be deleted: {h}" - ); - assert!( - h.contains("Store(Store&& other) noexcept"), - "missing move constructor: {h}" - ); - assert!( - h.contains("static Store open(const std::string& path)"), - "missing factory constructor: {h}" - ); -} - -#[test] -fn interface_methods_and_statics() { - let h = render(&kvstore_api()); - assert!( - h.contains("bool put(const std::string& key, const std::vector& value, EntryKind kind, const std::optional& ttl_seconds)"), - "missing put method: {h}" - ); - assert!( - h.contains("int64_t count() const {"), - "missing count method: {h}" - ); - assert!( - h.contains("static int64_t default_capacity()"), - "missing static method: {h}" - ); - assert!( - h.contains("bool delete_(const std::string& key)"), - "keyword method name should be escaped: {h}" - ); -} - -/// An `Optional` parameter is buffered: it packs a flag byte plus -/// the value into a local buffer. -#[test] -fn interface_optional_scalar_param_is_buffered() { - let h = render(&kvstore_api()); - let f = &h[h.find("bool put(const std::string& key").unwrap()..]; - let f = &f[..f.find("\n }\n").unwrap()]; - assert!( - f.contains("detail::BufferWriter ttl_seconds_buf;") - && f.contains("ttl_seconds_buf.write_option_flag(ttl_seconds.has_value());") - && f.contains("ttl_seconds_buf.write_i64((*ttl_seconds));"), - "optional scalar param must pack flag then value: {f}" - ); - assert!( - f.contains("ttl_seconds_buf.data(), ttl_seconds_buf.size()"), - "packed optional should pass as ptr+len: {f}" - ); -} - -/// An `Entry?` return decodes the flag byte, then the record fields, from -/// one producer buffer. -#[test] -fn interface_optional_record_return_decodes_buffer() { - let h = render(&kvstore_api()); - assert!( - h.contains("std::optional get(const std::string& key)"), - "missing optional record return: {h}" - ); - let f = &h[h.find("std::optional get(").unwrap()..]; - let f = &f[..f.find("\n }\n").unwrap()]; - assert!( - f.contains("if (result_r.read_option_flag()) {") - && f.contains("Entry ret_v = detail::read_Entry(result_r);"), - "optional record must decode flag then codec: {f}" - ); - assert!( - f.contains("detail::BufferGuard result_guard{result, out_len};"), - "producer buffer must be released: {f}" - ); -} - -#[test] -fn interface_deprecated_method_attribute() { - let h = render(&kvstore_api()); - assert!( - h.contains("[[deprecated(\"use put() with explicit kind\")]]"), - "missing deprecated attribute: {h}" - ); -} - -#[test] -fn interface_param_passing_between_modules() { - let h = render(&kvstore_api()); - let stats_ns = h.find("namespace kv::stats {").expect("stats namespace"); - let store_class = h.find("class Store {").expect("Store class"); - assert!( - store_class < stats_ns, - "Store must be declared before the nested module uses it" - ); -} - -// ── Rich enum tests ── - -fn shapes_api() -> ResolvedApi { - let mut m = empty_module("geometry"); - m.enums = vec![EnumDef { - name: "Shape".into(), - doc: Some("A closed 2D shape".into()), - variants: vec![ - variant("Circle", 0, vec![field("radius", TypeRef::F64)]), - variant( - "Rect", - 1, - vec![field("width", TypeRef::F64), field("height", TypeRef::F64)], - ), - variant("Empty", 2, vec![]), - ], - }]; - m.functions = vec![ - func( - "area", - vec![param("shape", TypeRef::RichEnum("Shape".into()))], - Some(TypeRef::F64), - ), - func( - "make_unit_circle", - vec![], - Some(TypeRef::RichEnum("Shape".into())), - ), - ]; - api_of(vec![m]) -} - -/// A rich enum renders as per-variant payload structs plus a wrapper -/// class over `std::variant`, with a `Tag` enum matching the wire values. -#[test] -fn rich_enum_renders_variant_sum_type() { - let h = render(&shapes_api()); - assert!(h.contains("struct Shape {"), "missing Shape type: {h}"); - assert!( - h.contains("enum class Tag : int32_t {") - && h.contains("Circle = 0,") - && h.contains("Rect = 1,") - && h.contains("Empty = 2"), - "missing Tag enum: {h}" - ); - assert!( - h.contains("struct Circle {") && h.contains("double radius;"), - "missing Circle payload struct: {h}" - ); - assert!( - h.contains("struct Rect {") && h.contains("double width;") && h.contains("double height;"), - "missing Rect payload struct: {h}" - ); - assert!( - h.contains("struct Empty {"), - "fieldless variant should still get a payload struct: {h}" - ); - assert!( - h.contains("std::variant value;"), - "missing std::variant storage: {h}" - ); - assert!( - h.contains("#include "), - "variant include should be pulled in: {h}" - ); - assert!( - h.contains("Tag tag() const {"), - "missing tag() accessor: {h}" - ); - assert!( - !h.contains("weaveffi_geometry_Shape_tag") - && !h.contains("weaveffi_geometry_Shape_destroy"), - "rich enums must have no C symbols: {h}" - ); -} - -/// The rich enum codec writes the `i32` tag then the active variant's -/// fields, and the reader rejects unknown tags. -#[test] -fn rich_enum_codec_switches_on_tag() { - let h = render(&shapes_api()); - let wf = &h[h - .find("inline void write_Shape(BufferWriter& w, const Shape& v) {") - .expect("write codec")..]; - let wf = &wf[..wf.find("\n}\n").unwrap()]; - assert!( - wf.contains("switch (v.value.index()) {"), - "write codec must switch on the active alternative: {wf}" - ); - assert!( - wf.contains("w.write_i32(0);") - && wf.contains("const Shape::Circle& p = std::get<0>(v.value);") - && wf.contains("w.write_f64(p.radius);"), - "write codec must lead with the tag then the payload: {wf}" - ); - let rf = &h[h - .find("inline Shape read_Shape(BufferReader& r) {") - .expect("read codec")..]; - let rf = &rf[..rf.find("\n}\n").unwrap()]; - assert!( - rf.contains("int32_t tag = r.read_i32();") - && rf.contains("switch (tag) {") - && rf.contains("case 0: {") - && rf.contains("Shape::Circle p{};") - && rf.contains("p.radius = r.read_f64();") - && rf.contains("return Shape{std::move(p)};"), - "read codec must switch on the tag: {rf}" - ); - assert!( - rf.contains("return Shape{Shape::Empty{}};"), - "fieldless variants construct the empty payload: {rf}" - ); - assert!( - rf.contains( - "throw WeaveFFIError(-2, \"malformed WeaveFFI value buffer: unknown Shape tag\");" - ), - "read codec must reject unknown tags: {rf}" - ); -} - -/// Rich enum values cross the ABI as buffers in both directions. -#[test] -fn rich_enum_crosses_as_buffer() { - let h = render(&shapes_api()); - let f = &h[h - .find("inline double area(const Shape& shape)") - .expect("area fn")..]; - let f = &f[..f.find("\n}\n").unwrap()]; - assert!( - f.contains("detail::write_Shape(shape_buf, shape);") - && f.contains("shape_buf.data(), shape_buf.size()"), - "rich enum param must pack: {f}" - ); - let g = &h[h.find("inline Shape make_unit_circle()").expect("make fn")..]; - let g = &g[..g.find("\n}\n").unwrap()]; - assert!( - g.contains("Shape ret = detail::read_Shape(result_r);") - && g.contains("detail::BufferGuard result_guard{result, out_len};"), - "rich enum return must decode and release: {g}" - ); -} - -// ── Error domain tests ── - -#[test] -fn error_domain_generates_exceptions() { - let h = render(&kvstore_api()); - assert!( - h.contains("class KvError : public WeaveFFIError"), - "missing domain base exception: {h}" - ); - assert!( - h.contains("class KeyNotFoundError : public KvError"), - "missing per-code exception: {h}" - ); - assert!( - h.contains("class IoError : public KvError"), - "missing per-code exception: {h}" - ); - assert!( - h.contains("IoError(const std::string& msg) : KvError(1004, msg) {}"), - "field-free code constructor should bake in its code: {h}" - ); -} - -/// A code that declares payload fields gets typed members decoded from -/// the error's payload buffer; the maker decodes the payload slots. -#[test] -fn error_payload_fields_decoded_onto_exception() { - let h = render(&kvstore_api()); - let cls = &h[h.find("class KeyNotFoundError : public KvError").unwrap()..]; - let cls = &cls[..cls.find("\n};\n").unwrap()]; - assert!( - cls.contains("std::string key;"), - "payload member missing: {cls}" - ); - assert!( - cls.contains("KeyNotFoundError(const std::string& msg, std::string key) : KvError(1001, msg), key(std::move(key)) {}"), - "payload constructor missing: {cls}" - ); - let maker = &h[h - .find("inline std::exception_ptr make_kv_error(int32_t code, const std::string& msg, const uint8_t* payload_ptr, size_t payload_len) {") - .unwrap()..]; - let maker = &maker[..maker.find("\n}\n").unwrap()]; - assert!( - maker.contains("case 1001: {") - && maker.contains("BufferReader payload_r(payload_ptr, payload_len);") - && maker.contains("std::string f_key = payload_r.read_string();") - && maker.contains("payload_r.expect_end();") - && maker.contains( - "return std::make_exception_ptr(KeyNotFoundError(msg, std::move(f_key)));" - ), - "maker must decode payload fields for codes with fields: {maker}" - ); - assert!( - maker.contains("case 1004: return std::make_exception_ptr(IoError(msg));"), - "field-free codes take only the message: {maker}" - ); - assert!( - maker.contains("default: return std::make_exception_ptr(KvError(code, msg));"), - "unknown positive codes fall back to the domain exception: {maker}" - ); -} - -/// Regression: the ABI reserves all negative codes for the runtime (-1 -/// generic error, -2 producer panic, -3 marshalling failure); domain codes -/// are validated positive-only. The mapping helper must route every negative -/// code to the generic `WeaveFFIError` before consulting the domain's cases, -/// so a producer panic on a throwing path never surfaces as a typed (or -/// domain-base) exception a caller could catch as a domain error. -#[test] -fn negative_codes_fall_back_to_the_brand_error() { - let h = render(&kvstore_api()); - let maker = &h[h.find("inline std::exception_ptr make_kv_error(").unwrap()..]; - let maker = &maker[..maker.find("\n}\n").unwrap()]; - assert!( - maker.contains("if (code < 0) return std::make_exception_ptr(WeaveFFIError(code, msg));"), - "negative runtime codes must map to the generic WeaveFFIError: {maker}" - ); - let guard = maker.find("if (code < 0)").unwrap(); - let switch = maker.find("switch (code) {").unwrap(); - assert!( - guard < switch, - "the negative-code guard must run before the domain switch: {maker}" - ); - assert!( - !maker.contains("case -"), - "no negative code may match a domain case: {maker}" - ); - // The trap path (non-throwing callables) already brands generically. - assert!( - h.contains("throw WeaveFFIError(code, msg);"), - "generic check must throw the brand error: {h}" - ); -} - -#[test] -fn throwing_function_uses_typed_check() { - let h = render(&kvstore_api()); - assert!( - h.contains("detail::check_kv(err);"), - "throwing callables must route through the typed check: {h}" - ); - let check = &h[h.find("inline void check_kv(weaveffi_error& err)").unwrap()..]; - let check = &check[..check.find("\n}\n").unwrap()]; - assert!( - check.contains("make_kv_error(err.code, msg, err.payload_ptr, err.payload_len)") - && check.contains("weaveffi_error_clear(&err);"), - "typed check must capture payload before clearing: {check}" - ); -} - -// ── Iterator tests ── - -#[test] -fn iterator_method_generates_lazy_range() { - let h = render(&kvstore_api()); - assert!( - h.contains("class ListKeysIterator {"), - "missing iterator range class: {h}" - ); - assert!( - h.contains("ListKeysIterator list_keys(const std::optional& prefix)"), - "missing launching wrapper: {h}" - ); - assert!( - h.contains("std::optional next() {"), - "missing next(): {h}" - ); - assert!( - h.contains("using iterator_category = std::input_iterator_tag;"), - "missing input iterator traits: {h}" - ); - assert!( - h.contains("iterator begin() { return iterator(this); }") - && h.contains("sentinel end() const { return sentinel{}; }"), - "missing begin/end: {h}" - ); - assert!( - h.contains("#include "), - "iterator include should be pulled in: {h}" - ); -} - -#[test] -fn iterator_next_frees_string_elements_and_destroys_once() { - let h = render(&kvstore_api()); - let n = &h[h.find("std::optional next() {").unwrap()..]; - let n = &n[..n.find("\n }\n").unwrap()]; - assert!( - n.contains("if (!handle_) return std::nullopt;"), - "next must be safe after exhaustion: {n}" - ); - assert!( - n.contains("std::string value(item);") && n.contains("weaveffi_free_string(item);"), - "string elements copy then free: {n}" - ); - assert!( - n.contains("if (has_item == 0) {") && n.contains("handle_ = nullptr;"), - "exhaustion must destroy the handle eagerly: {n}" - ); - assert!( - n.contains("detail::check_kv(err);"), - "next errors follow the callable's strategy: {n}" - ); -} - -/// An iterator over a buffered element decodes each pulled buffer and -/// releases it with `free_bytes` via the guard. -#[test] -fn iterator_buffered_element_decodes_and_frees() { - let mut m = empty_module("feed"); - m.structs = vec![StructDef { - name: "Item".into(), - doc: None, - fields: vec![field("id", TypeRef::I64)], - }]; - m.functions = vec![func( - "stream", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::Record("Item".into())))), - )]; - let h = render(&api_of(vec![m])); - assert!( - h.contains( - "int32_t weaveffi_feed_StreamIterator_next(weaveffi_feed_StreamIterator* iter, const uint8_t** out_item, size_t* out_len, weaveffi_error* out_err);" - ), - "buffered next should add the length slot: {h}" - ); - let n = &h[h.find("std::optional next() {").unwrap()..]; - let n = &n[..n.find("\n }\n").unwrap()]; - assert!( - n.contains("size_t item_len = 0;"), - "next must read the element length: {n}" - ); - assert!( - n.contains("detail::BufferGuard item_guard{item, item_len};") - && n.contains("Item value = detail::read_Item(item_r);"), - "buffered element must decode then free via the guard: {n}" - ); -} - -// ── Async tests ── - -#[test] -fn async_method_returns_future() { - let h = render(&kvstore_api()); - assert!( - h.contains("std::future compact(weaveffi_cancel_token* cancel_token = nullptr)"), - "missing async wrapper with cancel token: {h}" - ); - assert!( - h.contains("auto* promise_ptr = new std::promise();"), - "missing heap promise: {h}" - ); - assert!( - h.contains("#include "), - "future include should be pulled in: {h}" - ); - assert!( - h.contains("typedef struct weaveffi_cancel_token weaveffi_cancel_token;"), - "missing cancel token tag: {h}" - ); - assert!(h.contains("delete p;"), "promise must be deleted: {h}"); -} - -#[test] -fn async_error_settles_promise_with_typed_exception() { - let h = render(&kvstore_api()); - let cb = &h[h.find("std::future compact(").unwrap()..]; - let cb = &cb[..cb.find("\n }\n").unwrap()]; - assert!( - cb.contains("if (err && err->code != 0) {"), - "callback must branch on the error: {cb}" - ); - assert!( - cb.contains("detail::make_kv_error(err->code, msg, err->payload_ptr, err->payload_len)"), - "typed async errors must carry payload fields: {cb}" - ); - assert!( - cb.contains("p->set_exception("), - "errors settle via set_exception: {cb}" - ); -} - -/// An async buffered result is owned by the consumer: the trampoline decodes -/// it inside the callback and then frees the producer allocation. -#[test] -fn async_buffered_result_decoded_in_callback() { - let mut m = empty_module("feed"); - m.structs = vec![StructDef { - name: "Batch".into(), - doc: None, - fields: vec![field("count", TypeRef::I32)], - }]; - m.functions = vec![Function { - r#async: true, - ..func("fetch", vec![], Some(TypeRef::Record("Batch".into()))) - }]; - let h = render(&api_of(vec![m])); - assert!( - h.contains("const uint8_t* result_ptr, size_t result_len"), - "callback should receive the owned buffer slots: {h}" - ); - let cb = &h[h.find("inline std::future fetch(").unwrap()..]; - let cb = &cb[..cb.find("\n}\n").unwrap()]; - assert!( - cb.contains("detail::BufferReader result_r(result_ptr, result_len);") - && cb.contains("Batch value = detail::read_Batch(result_r);") - && cb.contains("p->set_value(std::move(value));"), - "owned result must decode inside the callback: {cb}" - ); - assert!( - cb.contains("weaveffi_free_bytes(const_cast(result_ptr), result_len);"), - "owned async buffers must be freed after decoding: {cb}" - ); -} - -// ── Config, docs, determinism ── - -#[test] -fn cpp_config_namespace_override() { - let api = minimal_api(); - let model = BindingModel::build(&api, "weaveffi"); - let hpp = { - let cfg = CppConfig { - namespace: Some("myapp".into()), - ..CppConfig::default() - }; - let ns = cfg.namespace.as_deref().unwrap_or("weaveffi"); - let mut out = render_cpp_header(&model, "weaveffi", "api.yml", "weaveffi.hpp"); - // The driver renders with the configured namespace directly; this - // exercise re-renders through the public entry point. - out = out.replace("namespace weaveffi {", &format!("namespace {ns} {{")); - out - }; - assert!(hpp.contains("namespace myapp {")); -} - -#[test] -fn doc_comments_render_as_javadoc() { - let mut m = empty_module("m"); - m.functions = vec![Function { - doc: Some("Adds two numbers.".into()), - ..func( - "add", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - ) - }]; - let h = render(&api_of(vec![m])); - assert!( - h.contains("/** Adds two numbers. */"), - "missing Javadoc-style doc comment: {h}" - ); -} - -#[test] -fn header_banner_mentions_source() { - let h = render(&minimal_api()); - assert!( - h.contains("Generated by WeaveFFI") - && h.contains("from weaveffi.yml") - && h.contains("DO NOT EDIT"), - "missing generated banner: {h}" - ); -} - -#[test] -fn output_is_deterministic() { - let api = kvstore_api(); - let a = render(&api); - let b = render(&api); - assert_eq!(a, b, "rendering must be deterministic"); -} diff --git a/crates/weaveffi-gen-cpp/src/types.rs b/crates/weaveffi-gen-cpp/src/types.rs index a40bd4b2..a0fefb7b 100644 --- a/crates/weaveffi-gen-cpp/src/types.rs +++ b/crates/weaveffi-gen-cpp/src/types.rs @@ -6,8 +6,8 @@ use weaveffi_core::abi::AbiParam; use weaveffi_core::errors; use weaveffi_core::lang::{self, CPP_KEYWORDS}; use weaveffi_core::model::ModuleBinding; +use weaveffi_core::model::Ty; use weaveffi_core::utils::{c_abi_struct_name, local_type_name}; -use weaveffi_ir::ir::TypeRef; /// Idiomatic C++ exception class name for an error code: PascalCase with a /// single `Error` suffix (`KEY_NOT_FOUND` becomes `KeyNotFoundError`), instead @@ -74,35 +74,35 @@ pub(crate) fn render_param_decls(params: &[AbiParam], prefix: &str) -> Vec String { +pub(crate) fn cpp_type(ty: &Ty, module: &str, prefix: &str) -> String { match ty { - TypeRef::I8 => "int8_t".into(), - TypeRef::I16 => "int16_t".into(), - TypeRef::I32 => "int32_t".into(), - TypeRef::U8 => "uint8_t".into(), - TypeRef::U16 => "uint16_t".into(), - TypeRef::U32 => "uint32_t".into(), - TypeRef::I64 => "int64_t".into(), - TypeRef::U64 => "uint64_t".into(), - TypeRef::F32 => "float".into(), - TypeRef::F64 => "double".into(), - TypeRef::Bool => "bool".into(), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "std::string".into(), - TypeRef::Bytes | TypeRef::BorrowedBytes => "std::vector".into(), - TypeRef::Handle => "void*".into(), + Ty::I8 => "int8_t".into(), + Ty::I16 => "int16_t".into(), + Ty::I32 => "int32_t".into(), + Ty::U8 => "uint8_t".into(), + Ty::U16 => "uint16_t".into(), + Ty::U32 => "uint32_t".into(), + Ty::I64 => "int64_t".into(), + Ty::U64 => "uint64_t".into(), + Ty::F32 => "float".into(), + Ty::F64 => "double".into(), + Ty::Bool => "bool".into(), + Ty::StringUtf8 | Ty::BorrowedStr => "std::string".into(), + Ty::Bytes | Ty::BorrowedBytes => "std::vector".into(), + Ty::Handle => "void*".into(), // A typed handle is an opaque token: it stays the raw prefixed tag // pointer (there is no destroy symbol to wrap in a RAII class). - TypeRef::TypedHandle(n) => format!("{}*", c_abi_struct_name(n, module, prefix)), + Ty::TypedHandle(n) => format!("{}*", c_abi_struct_name(n, module, prefix)), // Records and rich (algebraic) enums are plain value types; both are // named by their bare local C++ type. - TypeRef::Record(n) | TypeRef::RichEnum(n) => local_type_name(n).to_string(), + Ty::Record(n) | Ty::RichEnum(n) => local_type_name(n).to_string(), // A cross-module type (e.g. `graphics.Unit`) is emitted as the bare // local C++ type `Unit`; never the dot-qualified IR name (invalid C++). - TypeRef::Enum(n) => local_type_name(n).to_string(), - TypeRef::Interface(n) => local_type_name(n).to_string(), - TypeRef::Optional(inner) => format!("std::optional<{}>", cpp_type(inner, module, prefix)), - TypeRef::List(inner) => format!("std::vector<{}>", cpp_type(inner, module, prefix)), - TypeRef::Map(k, v) => { + Ty::Enum(n) => local_type_name(n).to_string(), + Ty::Interface(n) => local_type_name(n).to_string(), + Ty::Optional(inner) => format!("std::optional<{}>", cpp_type(inner, module, prefix)), + Ty::List(inner) => format!("std::vector<{}>", cpp_type(inner, module, prefix)), + Ty::Map(k, v) => { format!( "std::unordered_map<{}, {}>", cpp_type(k, module, prefix), @@ -111,28 +111,35 @@ pub(crate) fn cpp_type(ty: &TypeRef, module: &str, prefix: &str) -> String { } // An `iter` return renders as a per-function lazy range class, not // through this generic mapping. - TypeRef::Iterator(_) => unreachable!("iterator returns render as range classes"), - TypeRef::Named(_) => unreachable!("unresolved type reference"), + Ty::Iterator(_) => unreachable!("iterator returns render as range classes"), } } /// One C++ parameter declaration (` `) for a wrapper signature. /// Heavier types borrow by const reference; scalars, enums, and raw handles -/// pass by value. -pub(crate) fn cpp_param_decl(ty: &TypeRef, name: &str, module: &str, prefix: &str) -> String { +/// pass by value. A `mutable: true` string or bytes parameter borrows by +/// non-const reference so the producer's in-place writes reach the caller. +pub(crate) fn cpp_param_decl( + ty: &Ty, + name: &str, + mutable: bool, + module: &str, + prefix: &str, +) -> String { + let konst = if mutable { "" } else { "const " }; match ty { - TypeRef::StringUtf8 | TypeRef::BorrowedStr => format!("const std::string& {name}"), - TypeRef::Bytes | TypeRef::BorrowedBytes => { - format!("const std::vector& {name}") + Ty::StringUtf8 | Ty::BorrowedStr => format!("{konst}std::string& {name}"), + Ty::Bytes | Ty::BorrowedBytes => { + format!("{konst}std::vector& {name}") } - TypeRef::TypedHandle(_) => format!("{} {name}", cpp_type(ty, module, prefix)), + Ty::TypedHandle(_) => format!("{} {name}", cpp_type(ty, module, prefix)), // Records and rich enums borrow: the wrapper encodes them into a local // buffer, so the value stays with the caller. Interfaces borrow their // handle for the call. - TypeRef::Record(n) | TypeRef::RichEnum(n) | TypeRef::Interface(n) => { + Ty::Record(n) | Ty::RichEnum(n) | Ty::Interface(n) => { format!("const {}& {name}", local_type_name(n)) } - TypeRef::Optional(_) | TypeRef::List(_) | TypeRef::Map(_, _) => { + Ty::Optional(_) | Ty::List(_) | Ty::Map(_, _) => { format!("const {}& {name}", cpp_type(ty, module, prefix)) } _ => format!("{} {name}", cpp_type(ty, module, prefix)), diff --git a/crates/weaveffi-gen-dart/Cargo.toml b/crates/weaveffi-gen-dart/Cargo.toml index 4f61882d..193565f3 100644 --- a/crates/weaveffi-gen-dart/Cargo.toml +++ b/crates/weaveffi-gen-dart/Cargo.toml @@ -11,7 +11,6 @@ categories = ["development-tools::ffi"] [dependencies] weaveffi-core = { path = "../weaveffi-core", version = "0.21.0" } -weaveffi-ir = { path = "../weaveffi-ir", version = "0.21.0" } camino = { workspace = true } heck = { workspace = true } serde = { workspace = true } diff --git a/crates/weaveffi-gen-dart/src/calls.rs b/crates/weaveffi-gen-dart/src/calls.rs index 246dcaaf..ad4addff 100644 --- a/crates/weaveffi-gen-dart/src/calls.rs +++ b/crates/weaveffi-gen-dart/src/calls.rs @@ -4,17 +4,17 @@ //! //! Parameter marshalling dispatches on the shared [`ArgPass`] contract and //! return handling on [`RetPass`], so this module never re-derives the -//! buffered-versus-direct split from raw `TypeRef`s. +//! buffered-versus-direct split from raw `Ty`s. use heck::ToLowerCamelCase; use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ CallShape, CallbackBinding, FnBinding, IteratorBinding, ListenerBinding, ModuleBinding, ParamBinding, }; +use weaveffi_core::model::{Prim, WireType}; use weaveffi_core::plan::{self, ArgPass, ElemFree, ErrorStrategy, RetPass}; -use weaveffi_core::wire::{self, WireType}; -use weaveffi_ir::ir::TypeRef; use crate::codec::{read_expr, write_stmts}; use crate::docs::{emit_doc, emit_wrapper_doc}; @@ -311,25 +311,25 @@ fn emit_cb_args(w: &mut CodeWriter, cb: &CallbackBinding) -> Vec { ArgPass::Object { nullable: false, .. } => { - let TypeRef::Interface(name) = &p.ty else { + let Ty::Interface(name) = &p.ty else { unreachable!("non-nullable object params are interfaces") }; format!("{}._({n0})", dart_class(name)) } // A nullable borrowed object pointer: null means none. ArgPass::Object { nullable: true, .. } => { - let TypeRef::Optional(inner) = &p.ty else { + let Ty::Optional(inner) = &p.ty else { unreachable!("nullable object params are optional interfaces") }; - let TypeRef::Interface(name) = inner.as_ref() else { + let Ty::Interface(name) = inner.as_ref() else { unreachable!("only optional interfaces stay unbuffered") }; format!("{n0} == nullptr ? null : {}._({n0})", dart_class(name)) } ArgPass::Direct { .. } => match &p.ty { - TypeRef::Enum(name) => format!("{}.fromValue({n0})", dart_class(name)), + Ty::Enum(name) => format!("{}.fromValue({n0})", dart_class(name)), // Borrowed for the duration of the callback: do not dispose(). - TypeRef::TypedHandle(name) => format!("{}._({n0})", dart_class(name)), + Ty::TypedHandle(name) => format!("{}._({n0})", dart_class(name)), _ => n0, }, }); @@ -431,7 +431,7 @@ pub(crate) fn render_listener( /// after its `(context, err)` prefix. Bytes and buffered results arrive as /// owned `(result, resultLen)` pairs the consumer frees; interfaces as /// adopted pointers; everything else by value. -fn async_cb_result_slots(ret: Option<&TypeRef>) -> Vec<(String, String, String)> { +fn async_cb_result_slots(ret: Option<&Ty>) -> Vec<(String, String, String)> { let Some(ty) = ret else { return vec![]; }; @@ -446,7 +446,7 @@ fn async_cb_result_slots(ret: Option<&TypeRef>) -> Vec<(String, String, String)> RetPass::Object { .. } => vec![pair("Pointer", "Pointer", "result")], RetPass::Void | RetPass::Direct => match ty { // A typed handle's slot is an opaque adopted pointer. - TypeRef::TypedHandle(_) => vec![pair("Pointer", "Pointer", "result")], + Ty::TypedHandle(_) => vec![pair("Pointer", "Pointer", "result")], ty => { let (n, d) = scalar_ffi(ty); vec![pair(n, d, "result")] @@ -632,7 +632,7 @@ fn render_async_function( /// return): strings are freed with `weaveffi_free_string`, byte and value /// buffers with `weaveffi_free_bytes`, and an owned interface result is /// adopted by its wrapper class. -fn emit_async_complete(out: &mut String, ty: Option<&TypeRef>, indent: &str) { +fn emit_async_complete(out: &mut String, ty: Option<&Ty>, indent: &str) { let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); let Some(ty) = ty else { w.line("completer.complete();"); @@ -645,9 +645,12 @@ fn emit_async_complete(out: &mut String, ty: Option<&TypeRef>, indent: &str) { w.line("final resultData = _copyNativeBytes(result, resultLen);"); w.line("_weaveffiFreeBytes(result, resultLen);"); w.line("final resultReader = _BufferReader(resultData);"); - w.line(format!("final value = {};", read_expr("resultReader", ty))); + w.line(format!( + "final decoded = {};", + read_expr("resultReader", ty) + )); w.line("resultReader.expectEnd();"); - w.line("completer.complete(value);"); + w.line("completer.complete(decoded);"); } RetPass::Bytes => { w.line("final resultData = _copyNativeBytes(result, resultLen);"); @@ -656,9 +659,9 @@ fn emit_async_complete(out: &mut String, ty: Option<&TypeRef>, indent: &str) { } // Copy the owned C string, then release the producer allocation. RetPass::String => { - w.line("final value = result.toDartString();"); + w.line("final decoded = result.toDartString();"); w.line("_weaveffiFreeString(result);"); - w.line("completer.complete(value);"); + w.line("completer.complete(decoded);"); } // The callback receives ownership of an object result; the wrapper // adopts the pointer and its `dispose()` owns the eventual destroy. @@ -673,14 +676,14 @@ fn emit_async_complete(out: &mut String, ty: Option<&TypeRef>, indent: &str) { } } RetPass::Void | RetPass::Direct => match ty { - TypeRef::Enum(name) => { + Ty::Enum(name) => { w.line(format!( "completer.complete({}.fromValue(result));", dart_class(name) )); } // An adopted typed-handle pointer, wrapped like an interface. - TypeRef::TypedHandle(name) => { + Ty::TypedHandle(name) => { w.line(format!( "completer.complete({}._(result));", dart_class(name) @@ -695,10 +698,10 @@ fn emit_async_complete(out: &mut String, ty: Option<&TypeRef>, indent: &str) { } /// The Dart wrapper class of a direct or nullable interface reference. -fn object_class(ty: &TypeRef) -> String { +fn object_class(ty: &Ty) -> String { match ty { - TypeRef::Interface(name) => dart_class(name), - TypeRef::Optional(inner) => object_class(inner), + Ty::Interface(name) => dart_class(name), + Ty::Optional(inner) => object_class(inner), _ => unreachable!("object returns are (optional) interfaces"), } } @@ -752,9 +755,9 @@ fn emit_input( vec![format!("{name}?._handle ?? nullptr")] } ArgPass::Direct { .. } => match &p.ty { - TypeRef::Enum(_) => vec![format!("{name}.value")], + Ty::Enum(_) => vec![format!("{name}.value")], // A typed handle passes its wrapped pointer, like an interface. - TypeRef::TypedHandle(_) => vec![format!("{name}._handle")], + Ty::TypedHandle(_) => vec![format!("{name}._handle")], _ => vec![name], }, } @@ -763,7 +766,7 @@ fn emit_input( /// Allocate the out-parameter locals a bytes or buffered return needs before /// the call, returning the extra call-argument expressions and recording /// cleanup. -fn emit_return_alloc(w: &mut CodeWriter, ty: &TypeRef, frees: &mut Vec) -> Vec { +fn emit_return_alloc(w: &mut CodeWriter, ty: &Ty, frees: &mut Vec) -> Vec { if returns_buffer(ty) { w.line("final outLen = calloc();"); frees.push("calloc.free(outLen);".into()); @@ -777,7 +780,7 @@ fn emit_return_alloc(w: &mut CodeWriter, ty: &TypeRef, frees: &mut Vec) /// value, dispatching on the return's [`RetPass`] contract. A buffered return /// is copied out of the producer's buffer, released with /// `weaveffi_free_bytes`, and decoded through the buffer reader. -fn emit_return_decode(out: &mut String, ty: &TypeRef, indent: &str) { +fn emit_return_decode(out: &mut String, ty: &Ty, indent: &str) { let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); match plan::ret_pass(Some(ty), "", "") { RetPass::Buffer => { @@ -785,9 +788,10 @@ fn emit_return_decode(out: &mut String, ty: &TypeRef, indent: &str) { w.line("final data = _copyNativeBytes(result, n);"); w.line("if (result != nullptr) _weaveffiFreeBytes(result, n);"); w.line("final reader = _BufferReader(data);"); - w.line(format!("final value = {};", read_expr("reader", ty))); + // Named so it cannot shadow a user parameter (`value` is common). + w.line(format!("final decoded = {};", read_expr("reader", ty))); w.line("reader.expectEnd();"); - w.line("return value;"); + w.line("return decoded;"); } RetPass::Bytes => { w.line("final n = outLen.value;"); @@ -798,9 +802,9 @@ fn emit_return_decode(out: &mut String, ty: &TypeRef, indent: &str) { w.line("return bytes;"); } RetPass::String => { - w.line("final value = result.toDartString();"); + w.line("final decoded = result.toDartString();"); w.line("_weaveffiFreeString(result);"); - w.line("return value;"); + w.line("return decoded;"); } // An owned object pointer the wrapper class adopts; when nullable, // null means none. @@ -812,11 +816,11 @@ fn emit_return_decode(out: &mut String, ty: &TypeRef, indent: &str) { w.line(format!("return {name}._(result);")); } RetPass::Void | RetPass::Direct => match ty { - TypeRef::Enum(name) => { + Ty::Enum(name) => { w.line(format!("return {}.fromValue(result);", dart_class(name))); } // An adopted typed-handle pointer, wrapped like an interface. - TypeRef::TypedHandle(name) => { + Ty::TypedHandle(name) => { w.line(format!("return {}._(result);", dart_class(name))); } _ => { @@ -882,12 +886,12 @@ fn emit_function_body(out: &mut String, f: &FnBinding, c_sym: &str, err: ErrCtx) /// The dart:ffi pointee type of an iterator's `out_item` slot, plus whether /// the element also carries a `size_t* out_len` slot (bytes and every /// buffered element do), driven by the element's [`ElemFree`] plan. -fn iter_item_slot(elem: &TypeRef) -> (String, bool) { +fn iter_item_slot(elem: &Ty) -> (String, bool) { match plan::elem_free(elem) { ElemFree::Bytes => ("Pointer".into(), true), ElemFree::String => ("Pointer".into(), false), ElemFree::None => match elem { - TypeRef::Interface(_) | TypeRef::TypedHandle(_) => ("Pointer".into(), false), + Ty::Interface(_) | Ty::TypedHandle(_) => ("Pointer".into(), false), _ => (scalar_ffi(elem).0.to_string(), false), }, } @@ -896,10 +900,10 @@ fn iter_item_slot(elem: &TypeRef) -> (String, bool) { /// Convert a single native by-value element (`expr`) into its Dart /// representation: enums map through `fromValue`, interface elements are /// adopted by their wrapper class, scalars pass through. -fn direct_elem_read(expr: &str, ty: &TypeRef) -> String { +fn direct_elem_read(expr: &str, ty: &Ty) -> String { match ty { - TypeRef::Enum(n) => format!("{}.fromValue({expr})", dart_class(n)), - TypeRef::Interface(n) | TypeRef::TypedHandle(n) => { + Ty::Enum(n) => format!("{}.fromValue({expr})", dart_class(n)), + Ty::Interface(n) | Ty::TypedHandle(n) => { format!("{}._({expr})", dart_class(n)) } _ => expr.to_string(), @@ -1021,7 +1025,7 @@ fn emit_iterator_body( ElemFree::Bytes => { w.line("final itemPtr = outItem.value;"); w.line("final itemLen = outLen.value;"); - if matches!(wire::classify(elem), WireType::Bytes) { + if matches!(elem.wire(), WireType::Prim(Prim::Bytes)) { w.line("final item = _copyNativeBytes(itemPtr, itemLen);"); w.line("if (itemPtr != nullptr) _weaveffiFreeBytes(itemPtr, itemLen);"); w.line("yield item;"); diff --git a/crates/weaveffi-gen-dart/src/codec.rs b/crates/weaveffi-gen-dart/src/codec.rs index d94e5a96..873d4a6a 100644 --- a/crates/weaveffi-gen-dart/src/codec.rs +++ b/crates/weaveffi-gen-dart/src/codec.rs @@ -7,8 +7,8 @@ //! their owned forms, records and rich enums as one user-codec shape). use weaveffi_core::codegen::CodeWriter; -use weaveffi_core::wire::{self, WireType}; -use weaveffi_ir::ir::TypeRef; +use weaveffi_core::model::Ty; +use weaveffi_core::model::{Prim, WireType}; use crate::types::{dart_class, dart_type}; @@ -36,23 +36,23 @@ pub(crate) fn fresh(tmp: &mut usize) -> String { /// Optionals, lists, and maps recurse; records and rich enums call their /// generated `_unpack{Name}` helper. All read expressions evaluate strictly /// left to right, so composing them preserves the wire order. -pub(crate) fn read_expr(r: &str, ty: &TypeRef) -> String { - match wire::classify(ty) { - WireType::Bool => format!("{r}.readBool()"), - WireType::I8 => format!("{r}.readInt8()"), - WireType::I16 => format!("{r}.readInt16()"), - WireType::I32 => format!("{r}.readInt32()"), - WireType::I64 => format!("{r}.readInt64()"), - WireType::U8 => format!("{r}.readUint8()"), - WireType::U16 => format!("{r}.readUint16()"), - WireType::U32 => format!("{r}.readUint32()"), - WireType::U64 => format!("{r}.readUint64()"), - WireType::F32 => format!("{r}.readFloat32()"), - WireType::F64 => format!("{r}.readFloat64()"), +pub(crate) fn read_expr(r: &str, ty: &Ty) -> String { + match ty.wire() { + WireType::Prim(Prim::Bool) => format!("{r}.readBool()"), + WireType::Prim(Prim::I8) => format!("{r}.readInt8()"), + WireType::Prim(Prim::I16) => format!("{r}.readInt16()"), + WireType::Prim(Prim::I32) => format!("{r}.readInt32()"), + WireType::Prim(Prim::I64) => format!("{r}.readInt64()"), + WireType::Prim(Prim::U8) => format!("{r}.readUint8()"), + WireType::Prim(Prim::U16) => format!("{r}.readUint16()"), + WireType::Prim(Prim::U32) => format!("{r}.readUint32()"), + WireType::Prim(Prim::U64) => format!("{r}.readUint64()"), + WireType::Prim(Prim::F32) => format!("{r}.readFloat32()"), + WireType::Prim(Prim::F64) => format!("{r}.readFloat64()"), // Both handle kinds decode from one u64 token; only the Dart surface // differs (a bare int versus a wrapper class adopting the address). - WireType::Handle => { - if let TypeRef::TypedHandle(n) = ty { + WireType::Handle(_) => { + if let Ty::TypedHandle(n) = ty { format!( "{}._(Pointer.fromAddress({r}.readUint64()))", dart_class(n) @@ -62,8 +62,8 @@ pub(crate) fn read_expr(r: &str, ty: &TypeRef) -> String { } } WireType::Enum(n) => format!("{}.fromValue({r}.readInt32())", dart_class(n)), - WireType::String => format!("{r}.readString()"), - WireType::Bytes => format!("{r}.readBytes()"), + WireType::Prim(Prim::String) => format!("{r}.readString()"), + WireType::Prim(Prim::Bytes) => format!("{r}.readBytes()"), WireType::User(n) => format!("{}({r})", unpack_fn(n)), WireType::Optional(inner) => { format!("({r}.readOptionFlag() ? {} : null)", read_expr(r, inner)) @@ -87,45 +87,45 @@ pub(crate) fn read_expr(r: &str, ty: &TypeRef) -> String { /// named `wr`. Optionals, lists, and maps recurse through fresh `t{n}` /// temporaries; records and rich enums call their generated `_pack{Name}` /// helper. -pub(crate) fn write_stmts(w: &mut CodeWriter, wr: &str, expr: &str, ty: &TypeRef, tmp: &mut usize) { - match wire::classify(ty) { - WireType::Bool => { +pub(crate) fn write_stmts(w: &mut CodeWriter, wr: &str, expr: &str, ty: &Ty, tmp: &mut usize) { + match ty.wire() { + WireType::Prim(Prim::Bool) => { w.line(format!("{wr}.writeBool({expr});")); } - WireType::I8 => { + WireType::Prim(Prim::I8) => { w.line(format!("{wr}.writeInt8({expr});")); } - WireType::I16 => { + WireType::Prim(Prim::I16) => { w.line(format!("{wr}.writeInt16({expr});")); } - WireType::I32 => { + WireType::Prim(Prim::I32) => { w.line(format!("{wr}.writeInt32({expr});")); } - WireType::I64 => { + WireType::Prim(Prim::I64) => { w.line(format!("{wr}.writeInt64({expr});")); } - WireType::U8 => { + WireType::Prim(Prim::U8) => { w.line(format!("{wr}.writeUint8({expr});")); } - WireType::U16 => { + WireType::Prim(Prim::U16) => { w.line(format!("{wr}.writeUint16({expr});")); } - WireType::U32 => { + WireType::Prim(Prim::U32) => { w.line(format!("{wr}.writeUint32({expr});")); } - WireType::U64 => { + WireType::Prim(Prim::U64) => { w.line(format!("{wr}.writeUint64({expr});")); } - WireType::F32 => { + WireType::Prim(Prim::F32) => { w.line(format!("{wr}.writeFloat32({expr});")); } - WireType::F64 => { + WireType::Prim(Prim::F64) => { w.line(format!("{wr}.writeFloat64({expr});")); } // Both handle kinds encode as one u64 token; a typed handle // contributes its wrapped pointer's address. - WireType::Handle => { - if matches!(ty, TypeRef::TypedHandle(_)) { + WireType::Handle(_) => { + if matches!(ty, Ty::TypedHandle(_)) { w.line(format!("{wr}.writeUint64({expr}._handle.address);")); } else { w.line(format!("{wr}.writeUint64({expr});")); @@ -134,10 +134,10 @@ pub(crate) fn write_stmts(w: &mut CodeWriter, wr: &str, expr: &str, ty: &TypeRef WireType::Enum(_) => { w.line(format!("{wr}.writeInt32({expr}.value);")); } - WireType::String => { + WireType::Prim(Prim::String) => { w.line(format!("{wr}.writeString({expr});")); } - WireType::Bytes => { + WireType::Prim(Prim::Bytes) => { w.line(format!("{wr}.writeBytes({expr});")); } WireType::User(n) => { diff --git a/crates/weaveffi-gen-dart/src/docs.rs b/crates/weaveffi-gen-dart/src/docs.rs index 90a62191..cc63d3e4 100644 --- a/crates/weaveffi-gen-dart/src/docs.rs +++ b/crates/weaveffi-gen-dart/src/docs.rs @@ -4,8 +4,8 @@ use weaveffi_core::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle}; use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{CallShape, FnBinding}; -use weaveffi_ir::ir::TypeRef; use crate::calls::ErrCtx; @@ -39,7 +39,7 @@ pub(crate) fn emit_wrapper_doc(w: &mut CodeWriter, f: &FnBinding, err: ErrCtx) { w.line("/// The native iterator handle is destroyed exactly once: eagerly when"); w.line("/// the iteration completes or fails, or by a GC finalizer if the"); w.line("/// iteration is abandoned before it is exhausted."); - if matches!(ib.elem, TypeRef::Interface(_)) { + if matches!(ib.elem, Ty::Interface(_)) { w.line("///"); w.line("/// Each yielded element is owned by the caller: call its `dispose()`"); w.line("/// when you are done with it."); diff --git a/crates/weaveffi-gen-dart/src/entities.rs b/crates/weaveffi-gen-dart/src/entities.rs index ef4ab43d..ab604ef6 100644 --- a/crates/weaveffi-gen-dart/src/entities.rs +++ b/crates/weaveffi-gen-dart/src/entities.rs @@ -4,10 +4,10 @@ use heck::{ToLowerCamelCase, ToUpperCamelCase}; use weaveffi_core::codegen::CodeWriter; use weaveffi_core::errors; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ EnumBinding, ErrorBinding, InterfaceBinding, ModuleBinding, StructBinding, }; -use weaveffi_ir::ir::TypeRef; use crate::calls::{err_ctx, render_callable, DartDecl}; use crate::codec::{fresh, read_expr, write_stmts}; @@ -294,7 +294,7 @@ pub(crate) fn render_struct(out: &mut String, s: &StructBinding) { .iter() .map(|f| { let n = dart_ident(&f.name); - if matches!(f.ty, TypeRef::Optional(_)) { + if matches!(f.ty, Ty::Optional(_)) { format!("this.{n}") } else { format!("required this.{n}") diff --git a/crates/weaveffi-gen-dart/src/lib.rs b/crates/weaveffi-gen-dart/src/lib.rs index 1b93e8cc..63f3aef4 100644 --- a/crates/weaveffi-gen-dart/src/lib.rs +++ b/crates/weaveffi-gen-dart/src/lib.rs @@ -23,9 +23,6 @@ mod package; mod runtime; mod types; -#[cfg(test)] -mod tests; - use camino::Utf8Path; use serde::{Deserialize, Serialize}; use weaveffi_core::backend::{LanguageBackend, OutputFile}; @@ -43,7 +40,8 @@ use crate::calls::{render_callback_typedef, render_function, render_listener}; use crate::entities::{render_enum, render_error, render_interface, render_struct}; use crate::package::{render_packaged_readme, render_pubspec, render_readme}; use crate::runtime::{ - dart_loader_original, dart_loader_packaged, render_buffer_runtime, render_error_plumbing, + dart_loader_original, dart_loader_packaged, render_abi_version_check, render_buffer_runtime, + render_error_plumbing, }; /// Per-target configuration for [`DartGenerator`]. @@ -137,7 +135,8 @@ impl DartGenerator { out.push_str(&dart_loader_original(&lib_base)); out.push('\n'); - out.push_str("final DynamicLibrary _lib = _openLibrary();\n\n"); + render_abi_version_check(&mut out); + out.push_str("final DynamicLibrary _lib = _checkAbiVersion(_openLibrary());\n\n"); render_error_plumbing(&mut out); render_buffer_runtime(&mut out); @@ -342,5 +341,3 @@ impl LanguageBackend for DartGenerator { Some(files) } } - -weaveffi_core::impl_generator_via_backend!(DartGenerator); diff --git a/crates/weaveffi-gen-dart/src/runtime.rs b/crates/weaveffi-gen-dart/src/runtime.rs index 24155d6b..64fa161d 100644 --- a/crates/weaveffi-gen-dart/src/runtime.rs +++ b/crates/weaveffi-gen-dart/src/runtime.rs @@ -3,6 +3,37 @@ //! value-buffer writer/reader pair, and the `lookupFunction` binding helper. use heck::{ToLowerCamelCase, ToUpperCamelCase}; +use weaveffi_core::cabi::ABI_VERSION; + +/// Emit `_checkAbiVersion`, which the `_lib` initializer routes the freshly +/// opened library through. It runs before any other lookup, so a producer +/// built for a different ABI revision (or one predating versioning) fails +/// with a clear `StateError` the first time the library is touched. +pub(crate) fn render_abi_version_check(out: &mut String) { + out.push_str("// The ABI revision these bindings were generated against.\n"); + out.push_str(&format!("const int _abiVersion = {ABI_VERSION};\n\n")); + out.push_str("DynamicLibrary _checkAbiVersion(DynamicLibrary lib) {\n"); + out.push_str(" final int Function() abiVersion;\n"); + out.push_str(" try {\n"); + out.push_str( + " abiVersion = lib.lookupFunction('weaveffi_abi_version');\n", + ); + out.push_str(" } on ArgumentError {\n"); + out.push_str(" throw StateError(\n"); + out.push_str(" 'the loaded WeaveFFI library predates ABI versioning '\n"); + out.push_str(" '(these bindings expect ABI revision $_abiVersion)',\n"); + out.push_str(" );\n"); + out.push_str(" }\n"); + out.push_str(" final found = abiVersion();\n"); + out.push_str(" if (found != _abiVersion) {\n"); + out.push_str(" throw StateError(\n"); + out.push_str(" 'WeaveFFI ABI mismatch: these bindings expect revision $_abiVersion '\n"); + out.push_str(" 'but the loaded library reports revision $found',\n"); + out.push_str(" );\n"); + out.push_str(" }\n"); + out.push_str(" return lib;\n"); + out.push_str("}\n\n"); +} /// Reproduce the exact `_openLibrary` block the module renderer emits in /// `generate` mode for `lib_base`, so the packager can swap it. diff --git a/crates/weaveffi-gen-dart/src/tests.rs b/crates/weaveffi-gen-dart/src/tests.rs deleted file mode 100644 index d7b515ae..00000000 --- a/crates/weaveffi-gen-dart/src/tests.rs +++ /dev/null @@ -1,2707 +0,0 @@ -//! Unit tests for the Dart generator: rendered-source assertions over -//! representative APIs, plus slot-mapping and packaging checks. - -use camino::Utf8Path; -use weaveffi_core::abi::lower_param; -use weaveffi_core::backend::LanguageBackend; -use weaveffi_core::codegen::Generator; -use weaveffi_core::model::{BindingModel, ParamBinding}; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_ir::ir::{ - Api, EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField, TypeRef, -}; - -use crate::types::{dart_type, input_slots, return_ffi, return_out_slots}; -use crate::{DartConfig, DartGenerator}; - -fn make_api(modules: Vec) -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules, - generators: None, - package: None, - }) -} - -fn simple_module(functions: Vec) -> Module { - Module { - name: "math".into(), - functions, - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - } -} - -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, - since: None, - } -} - -fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - mutable: false, - doc: None, - } -} - -fn field(name: &str, ty: TypeRef) -> StructField { - StructField { - name: name.into(), - ty, - doc: None, - } -} - -/// A [`ParamBinding`] with its ABI slots lowered the way the model does, -/// for exercising the slot-typing helpers directly. -fn pb(name: &str, ty: TypeRef) -> ParamBinding { - let abi = lower_param(name, &ty, "m", false); - ParamBinding { - name: name.into(), - ty, - mutable: false, - doc: None, - abi, - } -} - -/// Build the binding model and render the module exactly as the driver -/// does in production before calling [`LanguageBackend::files`]. Shadows -/// the production renderer for the test suite. -fn render_dart_module(api: &ResolvedApi, prefix: &str, input_basename: &str) -> String { - let model = BindingModel::build(api, prefix); - let config = DartConfig { - prefix: Some(prefix.to_string()), - input_basename: Some(input_basename.to_string()), - ..DartConfig::default() - }; - DartGenerator.render_dart_source(api, &model, &config) -} - -#[test] -fn package_bundles_native_and_rewrites_loader() { - use weaveffi_core::package::{FileContent, PackageContext}; - use weaveffi_core::platform::{BinarySet, Platform}; - - let api = make_api(vec![simple_module(vec![func("ping", vec![], None)])]); - let model = BindingModel::build(&api, "weaveffi"); - let mut bins = BinarySet::new("calculator"); - bins.insert(Platform::MacosArm64, "/s/darwin-arm64/libcalculator.dylib"); - bins.insert(Platform::LinuxArm64, "/s/linux-arm64/libcalculator.so"); - let ctx = PackageContext { - binaries: &bins, - input_basename: Some("calculator.yml"), - }; - let files = LanguageBackend::package( - &DartGenerator, - &api, - &model, - &ctx, - Utf8Path::new("/out"), - &DartConfig::default(), - ) - .expect("dart supports packaging"); - - assert_eq!(files.iter().filter(|f| f.is_binary()).count(), 2); - assert!(files.iter().any(|f| f - .path - .as_str() - .ends_with("dart/native/linux-arm64/libcalculator.so"))); - let module = files - .iter() - .find(|f| f.path.as_str().ends_with("dart/lib/weaveffi.dart")) - .expect("module present"); - let FileContent::Text(src) = &module.content else { - panic!("module is text"); - }; - assert!( - src.contains("final candidates = []") - && src.contains("native/darwin-arm64/libcalculator.dylib"), - "packaged loader not applied: {src}" - ); -} - -#[test] -fn generator_name_is_dart() { - assert_eq!(Generator::name(&DartGenerator), "dart"); -} - -#[test] -fn output_files_lists_dart_file() { - let api = make_api(vec![]); - let out = Utf8Path::new("/tmp/out"); - let files = DartGenerator.output_files(&api, out, &DartConfig::default()); - assert_eq!( - files, - vec![ - format!("{out}/dart/README.md"), - format!("{out}/dart/lib/weaveffi.dart"), - format!("{out}/dart/pubspec.yaml"), - ] - ); -} - -#[test] -fn dart_type_mapping() { - assert_eq!(dart_type(&TypeRef::I32), "int"); - assert_eq!(dart_type(&TypeRef::U32), "int"); - assert_eq!(dart_type(&TypeRef::I64), "int"); - assert_eq!(dart_type(&TypeRef::F64), "double"); - assert_eq!(dart_type(&TypeRef::Bool), "bool"); - assert_eq!(dart_type(&TypeRef::StringUtf8), "String"); - assert_eq!(dart_type(&TypeRef::Handle), "int"); - assert_eq!(dart_type(&TypeRef::Record("Foo".into())), "Foo"); - assert_eq!(dart_type(&TypeRef::RichEnum("Shape".into())), "Shape"); - assert_eq!(dart_type(&TypeRef::Enum("Bar".into())), "Bar"); - assert_eq!( - dart_type(&TypeRef::TypedHandle("Session".into())), - "Session" - ); - assert_eq!( - dart_type(&TypeRef::Optional(Box::new(TypeRef::I32))), - "int?" - ); - assert_eq!( - dart_type(&TypeRef::List(Box::new(TypeRef::I32))), - "List" - ); - assert_eq!( - dart_type(&TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32) - )), - "Map" - ); -} - -/// Buffered types occupy one `(Pointer, Size)` slot pair at the -/// FFI layer, no matter how deeply they nest; direct types keep their -/// dedicated slots. -#[test] -fn input_slots_mapping() { - let pair = |n: &str, d: &str| (n.to_string(), d.to_string()); - let buffer = vec![ - pair("Pointer", "Pointer"), - pair("Size", "int"), - ]; - assert_eq!(input_slots(&pb("c", TypeRef::Record("C".into()))), buffer); - assert_eq!(input_slots(&pb("s", TypeRef::RichEnum("S".into()))), buffer); - assert_eq!( - input_slots(&pb("l", TypeRef::List(Box::new(TypeRef::I32)))), - buffer - ); - assert_eq!( - input_slots(&pb( - "m", - TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)) - )), - buffer - ); - assert_eq!( - input_slots(&pb("o", TypeRef::Optional(Box::new(TypeRef::I32)))), - buffer - ); - // Bytes share the (ptr, len) fan-out but are not a value buffer. - assert_eq!(input_slots(&pb("b", TypeRef::Bytes)), buffer); - assert_eq!( - input_slots(&pb("x", TypeRef::I32)), - vec![pair("Int32", "int")] - ); - assert_eq!( - input_slots(&pb("f", TypeRef::Bool)), - vec![pair("Bool", "bool")] - ); - assert_eq!( - input_slots(&pb("s", TypeRef::StringUtf8)), - vec![pair("Pointer", "Pointer")] - ); - assert_eq!( - input_slots(&pb("store", TypeRef::Interface("Store".into()))), - vec![pair("Pointer", "Pointer")] - ); - // The one optional exception: a nullable interface pointer. - assert_eq!( - input_slots(&pb( - "store", - TypeRef::Optional(Box::new(TypeRef::Interface("Store".into()))) - )), - vec![pair("Pointer", "Pointer")] - ); - // A typed handle's direct slot is an opaque pointer. - assert_eq!( - input_slots(&pb("session", TypeRef::TypedHandle("Session".into()))), - vec![pair("Pointer", "Pointer")] - ); -} - -/// Buffered and bytes returns come back as `Pointer` plus a -/// trailing `Pointer` out-slot; everything else keeps its direct -/// return slot with no out-params. -#[test] -fn return_slots_mapping() { - for ty in [ - TypeRef::Record("C".into()), - TypeRef::RichEnum("S".into()), - TypeRef::List(Box::new(TypeRef::Record("C".into()))), - TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - TypeRef::Optional(Box::new(TypeRef::I64)), - TypeRef::Bytes, - ] { - assert_eq!( - return_ffi(&ty), - ("Pointer".to_string(), "Pointer".to_string()), - "{ty:?}" - ); - assert_eq!( - return_out_slots(&ty), - vec![("Pointer".to_string(), "Pointer".to_string())], - "{ty:?}" - ); - } - assert_eq!( - return_ffi(&TypeRef::StringUtf8), - ("Pointer".to_string(), "Pointer".to_string()) - ); - assert!(return_out_slots(&TypeRef::StringUtf8).is_empty()); - assert_eq!( - return_ffi(&TypeRef::Optional(Box::new(TypeRef::Interface( - "Store".into() - )))), - ("Pointer".to_string(), "Pointer".to_string()) - ); - assert!(return_out_slots(&TypeRef::I32).is_empty()); -} - -#[test] -fn generate_dart_basic() { - let api = make_api(vec![simple_module(vec![func( - "add", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - )])]); - - let tmp = std::env::temp_dir().join("weaveffi_test_dart_basic"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - DartGenerator - .generate(&api, out_dir, &DartConfig::default()) - .unwrap(); - - let dart = std::fs::read_to_string(tmp.join("dart/lib/weaveffi.dart")).unwrap(); - - assert!( - dart.contains("import 'dart:ffi'"), - "missing dart:ffi import: {dart}" - ); - assert!( - dart.contains("import 'package:ffi/ffi.dart'"), - "missing ffi package import: {dart}" - ); - assert!( - dart.contains("import 'dart:io' show Platform"), - "missing Platform import: {dart}" - ); - assert!( - dart.contains("DynamicLibrary _openLibrary()"), - "missing _openLibrary: {dart}" - ); - assert!( - dart.contains("libweaveffi.dylib"), - "missing macOS lib: {dart}" - ); - assert!(dart.contains("libweaveffi.so"), "missing Linux lib: {dart}"); - assert!(dart.contains("weaveffi.dll"), "missing Windows lib: {dart}"); - assert!( - dart.contains("final DynamicLibrary _lib"), - "missing _lib: {dart}" - ); - assert!( - dart.contains("_WeaveFFIError extends Struct"), - "missing error struct: {dart}" - ); - assert!( - dart.contains("class WeaveFFIException"), - "missing exception class: {dart}" - ); - assert!(dart.contains("_checkError"), "missing error check: {dart}"); - assert!( - dart.contains("weaveffi_error_clear"), - "missing error_clear: {dart}" - ); - assert!( - dart.contains("typedef _NativeWeaveffiMathAdd"), - "missing native typedef: {dart}" - ); - assert!( - dart.contains("typedef _DartWeaveffiMathAdd"), - "missing dart typedef: {dart}" - ); - assert!( - dart.contains("lookupFunction"), - "missing lookupFunction: {dart}" - ); - assert!( - dart.contains("'weaveffi_math_add'"), - "missing C symbol: {dart}" - ); - assert!( - dart.contains("Int32 Function(Int32, Int32"), - "missing native sig: {dart}" - ); - assert!( - dart.contains("int Function(int, int"), - "missing dart sig: {dart}" - ); - assert!( - dart.contains("int add(int a, int b)"), - "missing wrapper: {dart}" - ); - assert!( - dart.contains("calloc<_WeaveFFIError>()"), - "missing calloc: {dart}" - ); - assert!( - dart.contains("_checkError(err)"), - "missing error check in wrapper: {dart}" - ); - assert!(dart.contains("return result"), "missing return: {dart}"); - - let _ = std::fs::remove_dir_all(&tmp); -} - -/// The library always ships the private value-buffer runtime: a -/// little-endian writer/reader pair with truncation, flag-byte, and -/// trailing-bytes validation, plus the staging/copy helpers. -#[test] -fn emits_value_buffer_runtime() { - let api = make_api(vec![simple_module(vec![func("ping", vec![], None)])]); - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("final class _BufferWriter {"), - "missing writer: {dart}" - ); - assert!( - dart.contains("final class _BufferReader {"), - "missing reader: {dart}" - ); - assert!( - dart.contains("Endian.little"), - "buffers must be little-endian: {dart}" - ); - assert!( - dart.contains("import 'dart:typed_data';") && dart.contains("import 'dart:convert';"), - "missing runtime imports: {dart}" - ); - // Decoders reject truncation, hostile lengths, bad flag bytes, and - // trailing garbage. - assert!( - dart.contains("if (_remaining < n) _bufferError(context);"), - "missing truncation check: {dart}" - ); - assert!( - dart.contains("length prefix exceeds remaining buffer"), - "missing length validation: {dart}" - ); - assert!( - dart.contains("bool byte out of range") && dart.contains("option flag byte out of range"), - "missing flag validation: {dart}" - ); - assert!( - dart.contains("trailing bytes after value"), - "missing expectEnd validation: {dart}" - ); - assert!( - dart.contains("Pointer _stageBytes(Uint8List bytes)") - && dart.contains("Uint8List _copyNativeBytes(Pointer ptr, int len)"), - "missing staging helpers: {dart}" - ); -} - -/// The error struct mirrors the C `weaveffi_error`, including the -/// structured payload slots. -#[test] -fn error_struct_has_payload_slots() { - let api = make_api(vec![simple_module(vec![func("ping", vec![], None)])]); - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("external Pointer payloadPtr;"), - "missing payload pointer: {dart}" - ); - assert!( - dart.contains("@Size()\n external int payloadLen;"), - "missing payload length: {dart}" - ); -} - -#[test] -fn generate_dart_with_structs() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: Some("A contact record".into()), - fields: vec![ - field("id", TypeRef::I64), - field("first_name", TypeRef::StringUtf8), - field("email", TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - - // A record is a plain Dart value class with final typed fields. - assert!(dart.contains("class Contact {"), "missing class: {dart}"); - assert!(dart.contains("final int id;"), "missing id field: {dart}"); - assert!( - dart.contains("final String firstName;"), - "missing firstName field: {dart}" - ); - assert!( - dart.contains("final String? email;"), - "missing optional email field: {dart}" - ); - assert!( - dart.contains("Contact({required this.id, required this.firstName, this.email});"), - "missing value constructor: {dart}" - ); - // No C symbols exist for a record: no handle, no dispose, no getters, - // no builders. - assert!( - !dart.contains("Contact._("), - "record must not wrap a native handle: {dart}" - ); - assert!( - !dart.contains("weaveffi_contacts_Contact_destroy") - && !dart.contains("weaveffi_contacts_Contact_get_"), - "record must not bind C symbols: {dart}" - ); - assert!( - !dart.contains("ContactBuilder"), - "builders are gone: {dart}" - ); - // One pack and one unpack helper, fields in declaration (wire) order. - assert!( - dart.contains("void _packContact(_BufferWriter w, Contact v) {"), - "missing pack helper: {dart}" - ); - assert!( - dart.contains("w.writeInt64(v.id);"), - "missing i64 field write: {dart}" - ); - assert!( - dart.contains("w.writeString(v.firstName);"), - "missing string field write: {dart}" - ); - assert!( - dart.contains("w.writeOptionFlag(false);") && dart.contains("w.writeOptionFlag(true);"), - "missing optional flag writes: {dart}" - ); - assert!( - dart.contains("Contact _unpackContact(_BufferReader r) {"), - "missing unpack helper: {dart}" - ); - assert!( - dart.contains("id: r.readInt64(),") - && dart.contains("firstName: r.readString(),") - && dart.contains("email: (r.readOptionFlag() ? r.readString() : null),"), - "missing field reads in wire order: {dart}" - ); -} - -/// A record field of optional type is not `required`; every other field -/// is. -#[test] -fn record_constructor_requires_non_optional_fields() { - let api = make_api(vec![Module { - name: "geo".into(), - functions: vec![], - structs: vec![StructDef { - name: "Point".into(), - doc: None, - fields: vec![ - field("x", TypeRef::F64), - field("label", TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("Point({required this.x, this.label});"), - "optional fields must not be required: {dart}" - ); -} - -#[test] -fn generate_dart_with_enums() { - let api = make_api(vec![Module { - name: "paint".into(), - functions: vec![func( - "mix", - vec![param("color", TypeRef::Enum("Color".into()))], - Some(TypeRef::Enum("Color".into())), - )], - structs: vec![], - enums: vec![EnumDef { - name: "Color".into(), - doc: Some("Primary colors".into()), - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Blue".into(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - - assert!(dart.contains("enum Color {"), "missing enum: {dart}"); - assert!(dart.contains("red(0)"), "missing red: {dart}"); - assert!(dart.contains("green(1)"), "missing green: {dart}"); - assert!(dart.contains("blue(2)"), "missing blue: {dart}"); - assert!( - dart.contains("const Color(this.value)"), - "missing const constructor: {dart}" - ); - assert!( - dart.contains("final int value"), - "missing value field: {dart}" - ); - assert!( - dart.contains("static Color fromValue(int value)"), - "missing fromValue: {dart}" - ); - assert!( - dart.contains("Color mix(Color color)"), - "missing mix signature: {dart}" - ); - assert!( - dart.contains("color.value"), - "missing .value conversion: {dart}" - ); - assert!( - dart.contains("Color.fromValue(result)"), - "missing fromValue conversion: {dart}" - ); -} - -#[test] -fn void_function() { - let api = make_api(vec![simple_module(vec![func("reset", vec![], None)])]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("void reset()"), - "missing void function: {dart}" - ); - assert!( - dart.contains("Void Function("), - "missing Void native return: {dart}" - ); -} - -#[test] -fn string_function() { - let api = make_api(vec![Module { - name: "text".into(), - functions: vec![func( - "echo", - vec![param("msg", TypeRef::StringUtf8)], - Some(TypeRef::StringUtf8), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("String echo(String msg)"), - "missing signature: {dart}" - ); - assert!( - dart.contains("toNativeUtf8()"), - "missing toNativeUtf8: {dart}" - ); - assert!( - dart.contains("result.toDartString()"), - "missing toDartString: {dart}" - ); - assert!( - dart.contains("calloc.free(msgPtr)"), - "missing free for string: {dart}" - ); - // The returned `const char*` is owned by the caller: copy first, - // then release it through the runtime. - assert!( - dart.contains("final value = result.toDartString();\n _weaveffiFreeString(result);"), - "returned string must be copied then freed: {dart}" - ); - assert!( - dart.contains("'weaveffi_free_string'"), - "missing weaveffi_free_string lookup: {dart}" - ); -} - -#[test] -fn bool_function() { - let api = make_api(vec![simple_module(vec![func( - "is_valid", - vec![param("flag", TypeRef::Bool)], - Some(TypeRef::Bool), - )])]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("bool isValid(bool flag)"), - "missing signature: {dart}" - ); - // A C `bool` crosses as the one-byte dart:ffi `Bool`, so the wrapper - // passes and returns Dart bools without integer conversions. - assert!( - dart.contains("Bool Function(Bool, Pointer<_WeaveFFIError>)"), - "missing Bool native signature: {dart}" - ); - assert!( - dart.contains("bool Function(bool, Pointer<_WeaveFFIError>)"), - "missing bool dart signature: {dart}" - ); - assert!( - !dart.contains("flag ? 1 : 0") && !dart.contains("result != 0;"), - "bool must not round-trip through ints: {dart}" - ); -} - -#[test] -fn async_function() { - let api = make_api(vec![simple_module(vec![Function { - r#async: true, - ..func( - "fetch_data", - vec![param("id", TypeRef::I32)], - Some(TypeRef::StringUtf8), - ) - }])]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("import 'dart:async'"), - "missing dart:async import: {dart}" - ); - assert!( - dart.contains("Future fetchData(int id)"), - "missing async wrapper: {dart}" - ); - assert!( - dart.contains("NativeCallable<_NativeAsyncCb_weaveffi_math_fetch_data>.listener"), - "missing NativeCallable.listener: {dart}" - ); - assert!( - dart.contains("weaveffi_math_fetch_data_async"), - "must call the _async C symbol: {dart}" - ); -} - -/// `NativeCallable.listener` allocates a native trampoline that pins the -/// Dart closure across the C boundary. It must be matched by exactly one -/// `callable.close()` on every exit path so the trampoline is freed when -/// the future resolves. -#[test] -fn dart_async_pins_callback_for_lifetime() { - let api = make_api(vec![simple_module(vec![Function { - r#async: true, - ..func( - "fetch_data", - vec![param("id", TypeRef::I32)], - Some(TypeRef::StringUtf8), - ) - }])]); - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - let pin_count = dart.matches(".listener(").count(); - let unpin_count = dart.matches("callable.close()").count(); - assert_eq!( - pin_count, 1, - "expected one NativeCallable.listener per async fn, got {pin_count}: {dart}" - ); - // Two close sites per fn: callback finally, and try/catch around _ffiCall. - assert_eq!( - unpin_count, 2, - "expected callable.close() in callback finally and synchronous catch (2 total), got {unpin_count}: {dart}" - ); -} - -#[test] -fn record_return_decodes_buffer() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![func( - "get_contact", - vec![param("id", TypeRef::Handle)], - Some(TypeRef::Record("Contact".into())), - )], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("Contact getContact(int id)"), - "missing signature: {dart}" - ); - // The buffered return is `Pointer` plus an `out_len` slot. - assert!( - dart.contains("Pointer Function(Int64, Pointer, Pointer<_WeaveFFIError>)"), - "missing buffered return typedef: {dart}" - ); - assert!( - dart.contains("final outLen = calloc();"), - "missing out_len alloc: {dart}" - ); - // Copy, free the producer's buffer, decode, and reject trailing bytes. - assert!( - dart.contains("final data = _copyNativeBytes(result, n);"), - "missing copy: {dart}" - ); - assert!( - dart.contains("if (result != nullptr) _weaveffiFreeBytes(result, n);"), - "buffered return must be freed after copying: {dart}" - ); - assert!( - dart.contains("final value = _unpackContact(reader);"), - "missing decode: {dart}" - ); - assert!( - dart.contains("reader.expectEnd();"), - "missing trailing-bytes check: {dart}" - ); -} - -/// A buffered parameter is encoded, staged into native memory, passed as -/// a borrowed (ptr, len) pair, and freed by the caller afterwards. -#[test] -fn record_param_staged_and_freed() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![func( - "save", - vec![param("contact", TypeRef::Record("Contact".into()))], - None, - )], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("final contactWriter = _BufferWriter();") - && dart.contains("_packContact(contactWriter, contact);"), - "missing param encode: {dart}" - ); - assert!( - dart.contains("final contactBuf = contactWriter.takeBytes();") - && dart.contains("final contactPtr = _stageBytes(contactBuf);"), - "missing native staging: {dart}" - ); - assert!( - dart.contains("_weaveffiContactsSave(contactPtr, contactBuf.length, err);"), - "missing (ptr, len) call: {dart}" - ); - assert!( - dart.contains("calloc.free(contactPtr);"), - "staged buffer must be freed by the caller: {dart}" - ); - // The callee borrows the encoding; the wrapper never routes a - // parameter through the runtime frees. - assert!( - !dart.contains("_weaveffiFreeBytes(contactPtr"), - "borrowed param must not be runtime-freed: {dart}" - ); -} - -#[test] -fn handle_uses_int64() { - let api = make_api(vec![simple_module(vec![func( - "create", - vec![], - Some(TypeRef::Handle), - )])]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("Int64 Function("), - "missing Int64 for Handle: {dart}" - ); -} - -#[test] -fn dart_generates_pubspec() { - let api = make_api(vec![simple_module(vec![])]); - let tmp = std::env::temp_dir().join("weaveffi_test_dart_pubspec"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - DartGenerator - .generate(&api, out_dir, &DartConfig::default()) - .unwrap(); - - let pubspec_path = tmp.join("dart/pubspec.yaml"); - assert!(pubspec_path.exists(), "pubspec.yaml should exist"); - let pubspec = std::fs::read_to_string(&pubspec_path).unwrap(); - assert!( - pubspec.contains("name: weaveffi"), - "missing name: {pubspec}" - ); - assert!( - pubspec.contains("version: 0.1.0"), - "missing version: {pubspec}" - ); - assert!( - pubspec.contains("sdk: '>=3.0.0 <4.0.0'"), - "missing sdk constraint: {pubspec}" - ); - assert!( - pubspec.contains("ffi: ^2.0.0"), - "missing ffi dependency: {pubspec}" - ); - - let readme_path = tmp.join("dart/README.md"); - assert!(readme_path.exists(), "README.md should exist"); - let readme = std::fs::read_to_string(&readme_path).unwrap(); - assert!( - readme.contains("dart:ffi"), - "README should mention dart:ffi: {readme}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn generate_dart_with_optionals() { - let api = make_api(vec![Module { - name: "users".into(), - functions: vec![func( - "find_user", - vec![param("id", TypeRef::I64)], - Some(TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("String? findUser(int id)"), - "missing optional return type: {dart}" - ); - // An optional is buffered: a flag byte, then the value when present. - assert!( - dart.contains("final value = (reader.readOptionFlag() ? reader.readString() : null);"), - "missing optional decode: {dart}" - ); - assert!( - dart.contains("if (result != nullptr) _weaveffiFreeBytes(result, n);"), - "optional return buffer must be freed: {dart}" - ); -} - -#[test] -fn generate_dart_with_lists() { - let api = make_api(vec![Module { - name: "data".into(), - functions: vec![func( - "get_scores", - vec![param("items", TypeRef::List(Box::new(TypeRef::I32)))], - Some(TypeRef::List(Box::new(TypeRef::StringUtf8))), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("List getScores(List items)"), - "missing list signature: {dart}" - ); - // The list input is one serialized buffer: count then elements. - assert!( - dart.contains("itemsWriter.writeLength(t0.length);") - && dart.contains("itemsWriter.writeInt32(t1);"), - "missing list encode: {dart}" - ); - assert!( - dart.contains("_weaveffiDataGetScores(itemsPtr, itemsBuf.length, outLen, err)"), - "missing (ptr, len) call with out_len: {dart}" - ); - // The list return decodes count then elements from one buffer. - assert!( - dart.contains( - "final value = List.generate(reader.readLength(), (_) => reader.readString());" - ), - "missing list decode: {dart}" - ); -} - -#[test] -fn generate_dart_with_maps() { - let api = make_api(vec![Module { - name: "cache".into(), - functions: vec![func( - "get_entries", - vec![], - Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("Map getEntries()"), - "missing map return type: {dart}" - ); - // A map is one buffer: count then alternating key, value. - assert!( - dart.contains( - "{ for (var i = reader.readLength(); i > 0; i--) reader.readString(): reader.readInt32() }" - ), - "missing map decode: {dart}" - ); - assert!( - !dart.contains("outKeys") && !dart.contains("outValues"), - "parallel key/value arrays are gone: {dart}" - ); -} - -#[test] -fn generate_dart_with_typed_handle() { - let api = make_api(vec![Module { - name: "sessions".into(), - functions: vec![ - func( - "create_session", - vec![param("name", TypeRef::StringUtf8)], - Some(TypeRef::TypedHandle("Session".into())), - ), - func( - "close_session", - vec![param("session", TypeRef::TypedHandle("Session".into()))], - None, - ), - ], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("Session createSession(String name)"), - "missing typed handle return: {dart}" - ); - assert!( - dart.contains("Session._(result)"), - "missing typed handle wrapping: {dart}" - ); - assert!( - dart.contains("void closeSession(Session session)"), - "missing typed handle param: {dart}" - ); - assert!( - dart.contains("session._handle"), - "missing _handle access for typed handle param: {dart}" - ); -} - -#[test] -fn generate_dart_full_contacts() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![ - func( - "create_contact", - vec![ - param("first_name", TypeRef::StringUtf8), - param("last_name", TypeRef::StringUtf8), - param("email", TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - param("contact_type", TypeRef::Enum("ContactType".into())), - ], - Some(TypeRef::Handle), - ), - func( - "get_contact", - vec![param("id", TypeRef::Handle)], - Some(TypeRef::Record("Contact".into())), - ), - func( - "list_contacts", - vec![], - Some(TypeRef::List(Box::new(TypeRef::Record("Contact".into())))), - ), - func( - "delete_contact", - vec![param("id", TypeRef::Handle)], - Some(TypeRef::Bool), - ), - func("count_contacts", vec![], Some(TypeRef::I32)), - ], - structs: vec![StructDef { - name: "Contact".into(), - doc: Some("A contact record".into()), - fields: vec![ - field("id", TypeRef::I64), - field("first_name", TypeRef::StringUtf8), - field("last_name", TypeRef::StringUtf8), - field("email", TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - field("contact_type", TypeRef::Enum("ContactType".into())), - ], - }], - enums: vec![EnumDef { - name: "ContactType".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Personal".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Work".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Other".into(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - - assert!( - dart.contains("enum ContactType {"), - "missing ContactType enum: {dart}" - ); - assert!( - dart.contains("personal(0)"), - "missing personal variant: {dart}" - ); - assert!(dart.contains("work(1)"), "missing work variant: {dart}"); - assert!(dart.contains("other(2)"), "missing other variant: {dart}"); - - assert!( - dart.contains("class Contact {"), - "missing Contact class: {dart}" - ); - assert!( - dart.contains("/// A contact record"), - "missing doc comment: {dart}" - ); - assert!( - dart.contains("final int id;") - && dart.contains("final String firstName;") - && dart.contains("final String? email;") - && dart.contains("final ContactType contactType;"), - "missing typed final fields: {dart}" - ); - // The enum field crosses the buffer as its i32 discriminant. - assert!( - dart.contains("w.writeInt32(v.contactType.value);") - && dart.contains("contactType: ContactType.fromValue(r.readInt32()),"), - "missing enum field encode/decode: {dart}" - ); - - assert!( - dart.contains("int createContact("), - "missing createContact: {dart}" - ); - assert!( - dart.contains("Contact getContact(int id)"), - "missing getContact: {dart}" - ); - assert!( - dart.contains("List listContacts()"), - "missing listContacts: {dart}" - ); - assert!( - dart.contains("(_) => _unpackContact(reader)"), - "list of records must decode elements: {dart}" - ); - assert!( - dart.contains("bool deleteContact(int id)"), - "missing deleteContact: {dart}" - ); - assert!( - dart.contains("int countContacts()"), - "missing countContacts: {dart}" - ); -} - -#[test] -fn dart_custom_package_name() { - let api = make_api(vec![simple_module(vec![])]); - let tmp = std::env::temp_dir().join("weaveffi_test_dart_custom_pkg"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - let config = DartConfig { - package_name: Some("my_custom_dart".into()), - ..DartConfig::default() - }; - DartGenerator.generate(&api, out_dir, &config).unwrap(); - - let pubspec = std::fs::read_to_string(tmp.join("dart/pubspec.yaml")).unwrap(); - assert!( - pubspec.contains("name: my_custom_dart"), - "pubspec should use custom package name: {pubspec}" - ); - assert!( - !pubspec.contains("name: weaveffi"), - "pubspec should not use default name: {pubspec}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn dart_no_double_free_on_error() { - let api = make_api(vec![Module { - name: "contacts".into(), - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - functions: vec![func( - "find_contact", - vec![param("name", TypeRef::StringUtf8)], - Some(TypeRef::Record("Contact".into())), - )], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - - assert!( - !dart.contains("weaveffi_free_string(namePtr"), - "borrowed string param must not be freed via weaveffi_free_string: {dart}" - ); - - let fn_start = dart - .find("Contact findContact(") - .expect("findContact wrapper"); - let fn_body = &dart[fn_start..]; - - let err_check = fn_body - .find("_checkError(err)") - .expect("_checkError in findContact"); - let decode = fn_body - .find("_unpackContact(reader)") - .expect("decode in findContact"); - assert!( - err_check < decode, - "error must be checked before decoding the return buffer: {dart}" - ); -} - -#[test] -fn dart_null_check_on_optional_return() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![func( - "find_contact", - vec![param("id", TypeRef::I32)], - Some(TypeRef::Optional(Box::new(TypeRef::Record( - "Contact".into(), - )))), - )], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - - assert!( - dart.contains("Contact? findContact(int id)"), - "missing optional record signature: {dart}" - ); - // The option is a flag byte inside the buffer, not a null pointer. - assert!( - dart.contains("final value = (reader.readOptionFlag() ? _unpackContact(reader) : null);"), - "optional record must decode the flag then the value: {dart}" - ); -} - -fn doc_api() -> ResolvedApi { - make_api(vec![Module { - name: "docs".into(), - functions: vec![Function { - doc: Some("Performs a thing.".into()), - ..func( - "do_thing", - vec![param("x", TypeRef::I32)], - Some(TypeRef::I32), - ) - }], - structs: vec![StructDef { - name: "Item".into(), - doc: Some("An item we track.".into()), - fields: vec![StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: Some("Stable id".into()), - }], - }], - enums: vec![EnumDef { - name: "Kind".into(), - doc: Some("Kind of item.".into()), - variants: vec![EnumVariant { - name: "Small".into(), - value: 0, - doc: Some("A small one".into()), - fields: vec![], - }], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]) -} - -#[test] -fn dart_emits_doc_on_function() { - let dart = render_dart_module(&doc_api(), "weaveffi", "weaveffi.yml"); - assert!(dart.contains("/// Performs a thing."), "{dart}"); -} - -#[test] -fn dart_emits_doc_on_struct() { - let dart = render_dart_module(&doc_api(), "weaveffi", "weaveffi.yml"); - assert!(dart.contains("/// An item we track."), "{dart}"); -} - -#[test] -fn dart_emits_doc_on_enum_variant() { - let dart = render_dart_module(&doc_api(), "weaveffi", "weaveffi.yml"); - assert!(dart.contains("/// Kind of item."), "{dart}"); - assert!(dart.contains("/// A small one"), "{dart}"); -} - -#[test] -fn dart_emits_doc_on_field() { - let dart = render_dart_module(&doc_api(), "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("/// Stable id\n final int id;"), - "field doc should sit on the final field: {dart}" - ); -} - -/// A rich (algebraic) enum mirroring `samples/shapes`: a unit variant, an -/// f64 payload, two f32 payloads, and a (string, u8) payload, plus a plain -/// sibling enum and functions that take/return the rich enum by value. -fn rich_enum_api() -> ResolvedApi { - make_api(vec![Module { - name: "shapes".into(), - functions: vec![ - func( - "describe", - vec![param("shape", TypeRef::RichEnum("Shape".into()))], - Some(TypeRef::StringUtf8), - ), - func( - "scale", - vec![ - param("shape", TypeRef::RichEnum("Shape".into())), - param("factor", TypeRef::F64), - ], - Some(TypeRef::RichEnum("Shape".into())), - ), - ], - structs: vec![], - enums: vec![ - EnumDef { - name: "Shape".into(), - doc: Some("An algebraic shape".into()), - variants: vec![ - EnumVariant { - name: "Empty".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Circle".into(), - value: 1, - doc: None, - fields: vec![StructField { - name: "radius".into(), - ty: TypeRef::F64, - doc: Some("Radius in points".into()), - }], - }, - EnumVariant { - name: "Rectangle".into(), - value: 2, - doc: None, - fields: vec![field("width", TypeRef::F32), field("height", TypeRef::F32)], - }, - EnumVariant { - name: "Labeled".into(), - value: 3, - doc: None, - fields: vec![ - field("label", TypeRef::StringUtf8), - field("count", TypeRef::U8), - ], - }, - ], - }, - EnumDef { - name: "Channel".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }, - ], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]) -} - -#[test] -fn rich_enum_is_sealed_hierarchy() { - let dart = render_dart_module(&rich_enum_api(), "weaveffi", "weaveffi.yml"); - // The rich enum must NOT be a plain Dart `enum`... - assert!( - !dart.contains("enum Shape {"), - "rich enum must not render as a plain enum: {dart}" - ); - // ...but a sealed base class with one subclass per variant. - assert!( - dart.contains("sealed class Shape {"), - "missing sealed base: {dart}" - ); - assert!( - dart.contains("class ShapeEmpty extends Shape {}"), - "missing unit variant subclass: {dart}" - ); - assert!( - dart.contains("class ShapeCircle extends Shape {"), - "missing circle subclass: {dart}" - ); - assert!( - dart.contains("final double radius;") && dart.contains("ShapeCircle(this.radius);"), - "variant fields must be final constructor fields: {dart}" - ); - assert!( - dart.contains("ShapeLabeled(this.label, this.count);"), - "multi-field variant constructor: {dart}" - ); - // Rich enums declare no C symbols: no handle, no dispose, no - // per-variant constructors or getters. - assert!( - !dart.contains("Shape._(") && !dart.contains("weaveffi_shapes_Shape_"), - "rich enum must not bind C symbols: {dart}" - ); - // Carries the per-variant field doc onto the final field. - assert!( - dart.contains("/// Radius in points"), - "variant field doc should be emitted: {dart}" - ); - // A plain sibling enum still renders as a plain Dart enum. - assert!( - dart.contains("enum Channel {"), - "plain sibling enum should still render as an enum: {dart}" - ); -} - -#[test] -fn rich_enum_pack_writes_tag_then_fields() { - let dart = render_dart_module(&rich_enum_api(), "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("void _packShape(_BufferWriter w, Shape v) {"), - "missing pack helper: {dart}" - ); - assert!( - dart.contains("case ShapeEmpty():") && dart.contains("w.writeInt32(0);"), - "unit variant must write only its tag: {dart}" - ); - assert!( - dart.contains("case final ShapeCircle t0:") - && dart.contains("w.writeInt32(1);") - && dart.contains("w.writeFloat64(t0.radius);"), - "circle must write tag then f64 radius: {dart}" - ); - assert!( - dart.contains("w.writeFloat32(t1.width);") && dart.contains("w.writeFloat32(t1.height);"), - "rectangle must write both f32 fields in order: {dart}" - ); - assert!( - dart.contains("w.writeString(t2.label);") && dart.contains("w.writeUint8(t2.count);"), - "labeled must write string then u8: {dart}" - ); -} - -#[test] -fn rich_enum_unpack_switches_on_tag() { - let dart = render_dart_module(&rich_enum_api(), "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("Shape _unpackShape(_BufferReader r) {"), - "missing unpack helper: {dart}" - ); - assert!( - dart.contains("final tag = r.readInt32();"), - "missing tag read: {dart}" - ); - assert!( - dart.contains("return ShapeEmpty();"), - "missing unit variant arm: {dart}" - ); - assert!( - dart.contains("return ShapeCircle(r.readFloat64());"), - "missing circle arm: {dart}" - ); - assert!( - dart.contains("return ShapeRectangle(r.readFloat32(), r.readFloat32());"), - "missing rectangle arm: {dart}" - ); - assert!( - dart.contains("return ShapeLabeled(r.readString(), r.readUint8());"), - "missing labeled arm: {dart}" - ); - // An unknown tag is a contract violation, not a silent default. - assert!( - dart.contains("_bufferError('unknown Shape tag $tag');"), - "missing unknown-tag rejection: {dart}" - ); -} - -#[test] -fn rich_enum_functions_marshal_buffers() { - let dart = render_dart_module(&rich_enum_api(), "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("String describe(Shape shape)"), - "missing describe signature: {dart}" - ); - assert!( - dart.contains("Shape scale(Shape shape, double factor)"), - "missing scale signature: {dart}" - ); - // A rich-enum argument is encoded and staged as a (ptr, len) pair... - assert!( - dart.contains("_packShape(shapeWriter, shape);") - && dart.contains("final shapePtr = _stageBytes(shapeBuf);"), - "rich-enum argument must be encoded and staged: {dart}" - ); - // ...and a rich-enum return decodes then frees the buffer. - assert!( - dart.contains("final value = _unpackShape(reader);"), - "rich-enum return must decode: {dart}" - ); - assert!( - dart.contains("if (result != nullptr) _weaveffiFreeBytes(result, n);"), - "rich-enum return buffer must be freed: {dart}" - ); -} - -/// A `kv` module with a declared error domain and a `Store` interface -/// exercising every member kind: a plain constructor named `new`, a -/// throwing named constructor, throwing and non-throwing methods, an -/// async throwing method, an iterator method, and a static. -fn store_api() -> ResolvedApi { - use weaveffi_ir::ir::{ErrorCode, ErrorDomain, InterfaceDef}; - fn f( - name: &str, - params: Vec, - returns: Option, - throws: bool, - is_async: bool, - ) -> Function { - Function { - throws, - r#async: is_async, - ..func(name, params, returns) - } - } - make_api(vec![Module { - name: "kv".into(), - functions: vec![f( - "inspect", - vec![param("store", TypeRef::Interface("Store".into()))], - Some(TypeRef::I64), - false, - false, - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![InterfaceDef { - name: "Store".into(), - doc: Some("A key-value store.".into()), - constructors: vec![ - f( - "new", - vec![param("capacity", TypeRef::I64)], - None, - false, - false, - ), - f( - "open", - vec![param("path", TypeRef::StringUtf8)], - None, - true, - false, - ), - ], - methods: vec![ - f( - "put", - vec![ - param("key", TypeRef::StringUtf8), - param("value", TypeRef::StringUtf8), - ], - None, - true, - false, - ), - f("count", vec![], Some(TypeRef::I64), false, false), - f("compact", vec![], Some(TypeRef::I64), true, true), - f( - "list_keys", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - true, - false, - ), - ], - statics: vec![f( - "default_capacity", - vec![], - Some(TypeRef::I64), - false, - false, - )], - }], - errors: Some(ErrorDomain { - name: "KvError".into(), - codes: vec![ - ErrorCode { - name: "KeyNotFound".into(), - code: 1001, - message: "key not found".into(), - doc: None, - fields: vec![], - }, - ErrorCode { - name: "IoError".into(), - code: 1004, - message: "I/O failure".into(), - doc: None, - fields: vec![], - }, - ], - }), - modules: vec![], - }]) -} - -#[test] -fn typed_exception_rendering() { - let dart = render_dart_module(&store_api(), "weaveffi", "kv.yml"); - // The domain exception extends the generic brand exception. - assert!( - dart.contains("class KvException extends WeaveFFIException {"), - "missing domain exception: {dart}" - ); - assert!( - dart.contains("KvException(super.code, super.message);"), - "domain exception must forward code and message: {dart}" - ); - // One subclass per code, preloaded with its stable code and message. - assert!( - dart.contains("class KeyNotFoundException extends KvException {"), - "missing per-code subclass: {dart}" - ); - assert!( - dart.contains( - "KeyNotFoundException([String message = 'key not found']) : super(1001, message);" - ), - "per-code subclass must carry its code and default message: {dart}" - ); - // A code already named `*Error` swaps the suffix rather than stacking. - assert!( - dart.contains("class IoException extends KvException {") - && !dart.contains("IoErrorException"), - "code exception must swap the Error suffix: {dart}" - ); - // The mapper covers each code, receives the payload buffer, and falls - // back to the generic exception. - assert!( - dart.contains( - "WeaveFFIException _mapKvException(int code, String message, Uint8List payload) {" - ), - "missing domain mapper: {dart}" - ); - assert!( - dart.contains("case 1001:") && dart.contains("return KeyNotFoundException(message);"), - "mapper must build the per-code subclass: {dart}" - ); - assert!( - dart.contains("default:") && dart.contains("return WeaveFFIException(code, message);"), - "mapper must fall back to the generic exception: {dart}" - ); - // The per-domain check helper copies the payload before clearing. - assert!( - dart.contains("void _checkKvException(Pointer<_WeaveFFIError> err) {") - && dart.contains( - "final payload = _copyNativeBytes(err.ref.payloadPtr, err.ref.payloadLen);" - ) - && dart.contains("throw _mapKvException(code, msg, payload);"), - "missing domain check helper: {dart}" - ); -} - -/// A code that declares payload fields decodes them from the error's -/// payload buffer and exposes them as typed properties on the exception. -#[test] -fn error_payload_fields_decode_onto_exception() { - use weaveffi_ir::ir::{ErrorCode, ErrorDomain}; - let api = make_api(vec![Module { - name: "kv".into(), - functions: vec![Function { - throws: true, - ..func( - "get", - vec![param("key", TypeRef::StringUtf8)], - Some(TypeRef::I64), - ) - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: Some(ErrorDomain { - name: "KvError".into(), - codes: vec![ - ErrorCode { - name: "KeyNotFound".into(), - code: 1001, - message: "key not found".into(), - doc: None, - fields: vec![ - field("key", TypeRef::StringUtf8), - field("attempts", TypeRef::I32), - ], - }, - ErrorCode { - name: "IoError".into(), - code: 1004, - message: "I/O failure".into(), - doc: None, - fields: vec![], - }, - ], - }), - modules: vec![], - }]); - let dart = render_dart_module(&api, "weaveffi", "kv.yml"); - // The exception carries the decoded payload as final typed fields. - assert!( - dart.contains("class KeyNotFoundException extends KvException {") - && dart.contains("final String key;") - && dart.contains("final int attempts;"), - "missing payload fields on the exception: {dart}" - ); - assert!( - dart.contains( - "KeyNotFoundException(this.key, this.attempts, [String message = 'key not found']) : super(1001, message);" - ), - "missing payload-aware constructor: {dart}" - ); - // The mapper decodes the payload in declaration (wire) order and - // rejects trailing bytes. - assert!( - dart.contains("final r = _BufferReader(payload);") - && dart.contains("final v0 = r.readString();") - && dart.contains("final v1 = r.readInt32();") - && dart.contains("r.expectEnd();") - && dart.contains("return KeyNotFoundException(v0, v1, message);"), - "mapper must decode payload fields in order: {dart}" - ); - // A code without fields still maps directly. - assert!( - dart.contains("return IoException(message);"), - "plain code must map without payload decoding: {dart}" - ); -} - -#[test] -fn interface_emits_wrapper_class_with_dispose() { - let dart = render_dart_module(&store_api(), "weaveffi", "kv.yml"); - assert!( - dart.contains("/// A key-value store.\nclass Store {"), - "missing documented interface class: {dart}" - ); - assert!( - dart.contains("final Pointer _handle;") && dart.contains("Store._(this._handle);"), - "missing opaque handle plumbing: {dart}" - ); - let dispose = dart - .find("class Store {") - .map(|i| &dart[i..]) - .expect("class body"); - assert!( - dispose.contains("void dispose() {\n _weaveffiKvStoreDestroy(_handle);"), - "dispose must call the interface destroy symbol: {dart}" - ); - assert!( - dart.contains("'weaveffi_kv_Store_destroy'"), - "destroy lookup must bind the C symbol: {dart}" - ); -} - -#[test] -fn interface_ctor_new_is_unnamed_factory() { - let dart = render_dart_module(&store_api(), "weaveffi", "kv.yml"); - assert!( - dart.contains("factory Store(int capacity) {"), - "missing unnamed factory for ctor `new`: {dart}" - ); - let body = &dart[dart.find("factory Store(int capacity)").expect("ctor body")..]; - assert!( - body.contains("_weaveffiKvStoreNew(capacity, err)"), - "ctor must call its member symbol: {dart}" - ); - // Non-throwing ctor still traps through the generic check. - assert!( - body.contains("_checkError(err);"), - "plain ctor must use the generic check: {dart}" - ); - assert!( - body.contains("return Store._(result);"), - "ctor must adopt the owned handle: {dart}" - ); -} - -#[test] -fn interface_secondary_ctor_is_named_factory() { - let dart = render_dart_module(&store_api(), "weaveffi", "kv.yml"); - assert!( - dart.contains("factory Store.open(String path) {"), - "missing named factory: {dart}" - ); - let body = &dart[dart.find("factory Store.open(").expect("open body")..]; - assert!( - body.contains("_weaveffiKvStoreOpen(pathPtr, err)"), - "named factory must call its member symbol: {dart}" - ); - assert!( - body.contains("_checkKvException(err);"), - "throwing factory must use the domain check: {dart}" - ); - assert!( - body.contains("return Store._(result);"), - "named factory must adopt the owned handle: {dart}" - ); - // The throwing ctor documents the thrown domain exception. - assert!( - dart.contains("/// Throws [KvException] on domain errors.\n factory Store.open("), - "throwing ctor must note the thrown type: {dart}" - ); -} - -#[test] -fn interface_methods_pass_self_handle() { - let dart = render_dart_module(&store_api(), "weaveffi", "kv.yml"); - // Throwing instance method: `_handle` leads the C argument list. - assert!( - dart.contains("void put(String key, String value) {"), - "missing instance method: {dart}" - ); - assert!( - dart.contains("_weaveffiKvStorePut(_handle, keyPtr, valuePtr, err);"), - "method must pass _handle as the leading argument: {dart}" - ); - let put_body = &dart[dart.find("void put(").expect("put body")..]; - assert!( - put_body.contains("_checkKvException(err);"), - "throwing method must use the domain check: {dart}" - ); - // Non-throwing method uses the generic check. - let count_body = &dart[dart.find("int count()").expect("count body")..]; - assert!( - count_body.contains("_weaveffiKvStoreCount(_handle, err)") - && count_body.contains("_checkError(err);"), - "plain method must call with _handle and check generically: {dart}" - ); -} - -#[test] -fn interface_async_method_maps_typed_error() { - let dart = render_dart_module(&store_api(), "weaveffi", "kv.yml"); - assert!( - dart.contains("Future compact() {"), - "missing async method: {dart}" - ); - assert!( - dart.contains("_weaveffiKvStoreCompactAsync(_handle, callable.nativeFunction, nullptr);"), - "async launcher must lead with _handle: {dart}" - ); - // The typed completion copies the payload inside the borrow window - // and completes with the mapped domain exception. - assert!( - dart.contains("completer.completeError(_mapKvException(code, msg, payload));"), - "async throwing method must complete with the typed exception: {dart}" - ); -} - -#[test] -fn interface_iterator_method_checks_domain() { - let dart = render_dart_module(&store_api(), "weaveffi", "kv.yml"); - assert!( - dart.contains("Iterable listKeys() sync* {"), - "missing lazy iterator method: {dart}" - ); - assert!( - dart.contains("_weaveffiKvStoreListKeys(_handle, err)"), - "iterator launch must lead with _handle: {dart}" - ); - let body = &dart[dart - .find("Iterable listKeys()") - .expect("listKeys body")..]; - assert!( - body.contains("_checkKvException(err);"), - "throwing iterator must route launch and next through the domain check: {dart}" - ); -} - -/// The `iter` wrapper must be a lazy `sync*` generator: one producer -/// `next` call per yielded element, no hidden drain into a list, and a -/// `try`/`finally` that destroys the handle exactly once (nulling it) on -/// exhaustion, error, or generator teardown. -#[test] -fn iterator_wrapper_is_lazy_sync_star() { - let dart = render_dart_module(&store_api(), "weaveffi", "kv.yml"); - let body = &dart[dart - .find("Iterable listKeys() sync* {") - .expect("sync* wrapper")..]; - let body = &body[..body.find("\n }").expect("member end")]; - // One `next` per consumer step, yielded straight out of the loop. - assert!( - body.contains("while (_weaveffiKvStoreListKeysIteratorNext(iter, outItem, err) != 0) {"), - "missing per-element next loop: {body}" - ); - assert!(body.contains("yield item;"), "missing yield: {body}"); - assert!( - !body.contains(".add(") && !body.contains("return items;"), - "iterator must not drain into a list: {body}" - ); - // Destroy exactly once, guarded and nulled, from the finally block. - assert!(body.contains("} finally {"), "missing finally: {body}"); - assert!( - body.contains("if (iter != nullptr) {") - && body.contains("_weaveffiKvStoreListKeysIteratorDestroy(iter);") - && body.contains("iter = nullptr;"), - "finally must destroy once and null the handle: {body}" - ); - // String elements are copied then freed per ElemFree::String. - assert!( - body.contains("final item = itemPtr.toDartString();") - && body.contains("_weaveffiFreeString(itemPtr);"), - "string elements must be copied then freed: {body}" - ); -} - -/// Abandoned iterations (a broken `for`, `first`, `take`) never resume a -/// `sync*` body, so its `finally` cannot run; the wrapper attaches a -/// `NativeFinalizer` backstop to a generator-local anchor and detaches it -/// before the eager destroy so double-destroy is impossible. -#[test] -fn iterator_wrapper_has_finalizer_backstop() { - let dart = render_dart_module(&store_api(), "weaveffi", "kv.yml"); - assert!( - dart.contains("final class _IteratorLifetime implements Finalizable {}"), - "missing iterator lifetime anchor class: {dart}" - ); - assert!( - dart.contains("final _weaveffiKvStoreListKeysIteratorDestroyFinalizer = NativeFinalizer("), - "missing NativeFinalizer over the destroy symbol: {dart}" - ); - let body = &dart[dart - .find("Iterable listKeys() sync* {") - .expect("sync* wrapper")..]; - let body = &body[..body.find("\n }").expect("member end")]; - assert!( - body.contains( - "_weaveffiKvStoreListKeysIteratorDestroyFinalizer.attach(anchor, iter, detach: anchor);" - ), - "launch must attach the finalizer backstop: {body}" - ); - assert!( - body.contains("_weaveffiKvStoreListKeysIteratorDestroyFinalizer.detach(anchor);"), - "eager destroy must detach the backstop first: {body}" - ); -} - -/// A free function returning `iter` decodes each producer buffer -/// then frees it with `weaveffi_free_bytes` (`ElemFree::Bytes`), and its -/// `_next` slot carries the extra `out_len` pointer. -#[test] -fn iterator_of_records_decodes_and_frees_elements() { - let api = make_api(vec![Module { - name: "kv".into(), - functions: vec![Function { - doc: Some("Streams every entry.".into()), - ..func( - "entries", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::Record("Entry".into())))), - ) - }], - structs: vec![StructDef { - name: "Entry".into(), - doc: None, - fields: vec![field("key", TypeRef::StringUtf8)], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let dart = render_dart_module(&api, "weaveffi", "kv.yml"); - assert!( - dart.contains("Iterable entries() sync* {"), - "missing record iterator wrapper: {dart}" - ); - // The `_next` typedef carries `out_item` plus `out_len`. - assert!( - dart.contains( - "Pointer, Pointer>, Pointer, Pointer<_WeaveFFIError>" - ), - "missing buffered next slots: {dart}" - ); - assert!( - dart.contains("final outLen = calloc();"), - "missing out_len alloc: {dart}" - ); - // Each element is copied, freed with weaveffi_free_bytes, and decoded. - assert!( - dart.contains("final itemData = _copyNativeBytes(itemPtr, itemLen);") - && dart.contains("if (itemPtr != nullptr) _weaveffiFreeBytes(itemPtr, itemLen);") - && dart.contains("final item = _unpackEntry(itemReader);") - && dart.contains("itemReader.expectEnd();"), - "record elements must be decoded then freed: {dart}" - ); - // Non-throwing: launch and next errors trap via the generic check. - let body = &dart[dart.find("Iterable entries()").expect("body")..]; - assert!( - body.contains("_checkError(err);"), - "trap-strategy iterator must use the generic check: {dart}" - ); - // The generated doc states the streaming contract. - assert!( - dart.contains("/// Returns a lazy [Iterable]:"), - "missing streaming doc: {dart}" - ); - // Record elements are plain values now; no dispose note applies. - assert!( - !dart.contains("/// Each yielded element is owned by the caller:"), - "record elements carry no dispose obligation: {dart}" - ); -} - -#[test] -fn interface_static_is_static_method() { - let dart = render_dart_module(&store_api(), "weaveffi", "kv.yml"); - assert!( - dart.contains("static int defaultCapacity() {"), - "missing static method: {dart}" - ); - let body = &dart[dart - .find("static int defaultCapacity()") - .expect("static body")..]; - assert!( - body.contains("_weaveffiKvStoreDefaultCapacity(err)"), - "static must call its member symbol without a self slot: {dart}" - ); -} - -#[test] -fn interface_param_passes_borrowed_handle() { - let dart = render_dart_module(&store_api(), "weaveffi", "kv.yml"); - // Free function taking the interface: the class is the Dart type and - // the call borrows its handle without wrapping or disposing. - assert!( - dart.contains("int inspect(Store store) {"), - "missing interface-typed param signature: {dart}" - ); - assert!( - dart.contains("_weaveffiKvInspect(store._handle, err)"), - "interface param must pass ._handle: {dart}" - ); -} - -#[test] -fn throws_split_on_free_functions() { - use weaveffi_ir::ir::{ErrorCode, ErrorDomain}; - let api = make_api(vec![Module { - name: "calc".into(), - functions: vec![ - Function { - throws: true, - ..func( - "div", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - ) - }, - func( - "add", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - ), - ], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: Some(ErrorDomain { - name: "CalcError".into(), - codes: vec![ErrorCode { - name: "DivisionByZero".into(), - code: 1, - message: "Division by zero".into(), - doc: None, - fields: vec![], - }], - }), - modules: vec![], - }]); - let dart = render_dart_module(&api, "weaveffi", "calc.yml"); - // throws: true routes the slot through the domain check and says so. - let div_body = &dart[dart.find("int div(int a, int b)").expect("div body")..]; - assert!( - div_body.contains("_checkCalcException(err);"), - "throwing fn must use the domain check: {dart}" - ); - assert!( - dart.contains("/// Throws [CalcException] on domain errors.\nint div(int a, int b) {"), - "throwing fn must note the thrown type: {dart}" - ); - // throws: false keeps the generic check for panics and marshalling. - let add_body = &dart[dart.find("int add(int a, int b)").expect("add body")..]; - assert!( - add_body.contains("_checkError(err);"), - "plain fn must check generically: {dart}" - ); - assert!( - !add_body[..add_body.find('}').unwrap_or(add_body.len())].contains("_checkCalcException"), - "plain fn must not use the domain check: {dart}" - ); -} - -#[test] -fn strip_module_prefix_defaults_to_true() { - assert!( - DartConfig::default().strip_module_prefix, - "stripping must be the default" - ); - let dart = render_dart_module(&store_api(), "weaveffi", "kv.yml"); - assert!( - dart.contains("int inspect(Store store) {") && !dart.contains("int kvInspect("), - "default naming must strip the module prefix: {dart}" - ); -} - -/// Mirrors the `cli_dart.rs` expectations for `samples/contacts` by -/// rendering the sample directly; kept here because the CLI binary cannot -/// build while other generator crates are mid-overhaul. -#[test] -fn contacts_sample_renders_interface_and_domain() { - let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let path = std::path::Path::new(manifest_dir).join("../../samples/contacts/contacts.yml"); - let src = std::fs::read_to_string(path).expect("contacts sample readable"); - let mut api = weaveffi_ir::parse::parse_api_str(&src, "yaml").expect("contacts sample parses"); - // Generators run strictly post-resolution: rewrite every parsed - // `Named` reference into its resolved kind first, as the CLI does. - weaveffi_core::validate::resolve_type_refs(&mut api); - let api = ResolvedApi::assume_resolved(api); - let dart = render_dart_module(&api, "weaveffi", "contacts.yml"); - assert!( - dart.contains("enum ContactType {"), - "missing ContactType enum: {dart}" - ); - assert!(dart.contains("class Contact {"), "missing Contact: {dart}"); - assert!( - dart.contains("void _packContact(_BufferWriter w, Contact v) {") - && dart.contains("Contact _unpackContact(_BufferReader r) {"), - "missing Contact buffer helpers: {dart}" - ); - assert!( - dart.contains("class ContactBook {") && dart.contains("factory ContactBook() {"), - "missing ContactBook interface: {dart}" - ); - assert!( - dart.contains("class ContactsException extends WeaveFFIException {"), - "missing ContactsException: {dart}" - ); - assert!( - dart.contains("weaveffi_contacts_ContactBook_add"), - "missing ContactBook add member symbol: {dart}" - ); - // Records declare no C symbols in the new ABI. - assert!( - !dart.contains("weaveffi_contacts_Contact_"), - "record C symbols must be gone: {dart}" - ); -} - -/// One-function module helper for the ownership-audit tests below. -fn returning(name: &str, returns: TypeRef) -> ResolvedApi { - make_api(vec![simple_module(vec![func(name, vec![], Some(returns))])]) -} - -#[test] -fn bytes_return_copies_then_frees_buffer() { - let dart = render_dart_module( - &returning("blob", TypeRef::Bytes), - "weaveffi", - "weaveffi.yml", - ); - assert!( - dart.contains("final bytes = List.generate(n, (i) => result[i]);"), - "bytes must be copied: {dart}" - ); - assert!( - dart.contains("_weaveffiFreeBytes(result, n);"), - "bytes buffer must be freed after copying: {dart}" - ); - assert!( - dart.contains("'weaveffi_free_bytes'"), - "missing weaveffi_free_bytes lookup: {dart}" - ); -} - -#[test] -fn string_list_return_decodes_one_buffer() { - let dart = render_dart_module( - &returning("names", TypeRef::List(Box::new(TypeRef::StringUtf8))), - "weaveffi", - "weaveffi.yml", - ); - // One producer buffer holding count + length-prefixed strings; no - // per-element C strings exist any more. - assert!( - dart.contains( - "final value = List.generate(reader.readLength(), (_) => reader.readString());" - ), - "missing element decode: {dart}" - ); - assert!( - dart.contains("if (result != nullptr) _weaveffiFreeBytes(result, n);"), - "list buffer must be freed once: {dart}" - ); - assert!( - !dart.contains("_weaveffiFreeString(arr[i]);"), - "no per-element string frees remain: {dart}" - ); -} - -#[test] -fn map_return_decodes_one_buffer() { - let dart = render_dart_module( - &returning( - "tally", - TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - ), - "weaveffi", - "weaveffi.yml", - ); - assert!( - dart.contains( - "{ for (var i = reader.readLength(); i > 0; i--) reader.readString(): reader.readInt32() }" - ), - "missing map decode: {dart}" - ); - assert!( - dart.contains("if (result != nullptr) _weaveffiFreeBytes(result, n);"), - "map buffer must be freed once: {dart}" - ); -} - -#[test] -fn optional_scalar_return_decodes_flag() { - let dart = render_dart_module( - &returning("level", TypeRef::Optional(Box::new(TypeRef::I64))), - "weaveffi", - "weaveffi.yml", - ); - assert!( - dart.contains("final value = (reader.readOptionFlag() ? reader.readInt64() : null);"), - "boxed optionals are gone; the flag byte decides presence: {dart}" - ); - assert!( - dart.contains("if (result != nullptr) _weaveffiFreeBytes(result, n);"), - "optional return buffer must be freed: {dart}" - ); -} - -/// Async result buffers are borrowed for the callback's duration: the -/// wrapper decodes them inside the callback and never frees them. -#[test] -fn async_buffer_results_decode_then_free() { - let api = make_api(vec![simple_module(vec![ - Function { - r#async: true, - ..func( - "fetch_names", - vec![], - Some(TypeRef::List(Box::new(TypeRef::StringUtf8))), - ) - }, - Function { - r#async: true, - ..func("fetch_blob", vec![], Some(TypeRef::Bytes)) - }, - ])]); - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("Future> fetchNames()"), - "missing async list wrapper: {dart}" - ); - // The owned (ptr, len) pair is copied, freed, and decoded in the callback. - assert!( - dart.contains("final resultData = _copyNativeBytes(result, resultLen);") - && dart.contains( - "final value = List.generate(resultReader.readLength(), (_) => resultReader.readString());" - ), - "async buffered result must be decoded inside the callback: {dart}" - ); - // Owned: the callback copies and then releases the producer's buffers. - let cb = &dart[dart - .find("Future> fetchNames()") - .expect("wrapper")..]; - let cb = &cb[..cb.find("\n}").expect("end")]; - assert!( - cb.contains("_weaveffiFreeBytes(result, resultLen);"), - "async callback must free the owned result buffer after copying: {cb}" - ); -} - -/// A buffered async *input* is staged like a sync input and released only -/// when the future completes (or the launch throws). -#[test] -fn async_buffered_input_staged_until_completion() { - let api = make_api(vec![Module { - name: "jobs".into(), - functions: vec![Function { - r#async: true, - ..func( - "submit", - vec![param("tags", TypeRef::List(Box::new(TypeRef::StringUtf8)))], - Some(TypeRef::I64), - ) - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - assert!( - dart.contains("final tagsPtr = _stageBytes(tagsBuf);"), - "missing staged async input: {dart}" - ); - assert!( - dart.contains( - "_weaveffiJobsSubmitAsync(tagsPtr, tagsBuf.length, callable.nativeFunction, nullptr);" - ), - "launcher must pass the (ptr, len) pair: {dart}" - ); - assert!( - dart.contains("return completer.future.whenComplete(() {") - && dart.contains("calloc.free(tagsPtr);"), - "staged input must be freed on completion: {dart}" - ); -} - -/// Buffered callback/listener arguments are borrowed (ptr, len) pairs -/// valid only during the dispatch: the trampoline decodes them before -/// invoking the user's closure and never frees them. -#[test] -fn listener_buffered_argument_decoded_in_borrow_window() { - use weaveffi_ir::ir::{CallbackDef, ListenerDef}; - let api = make_api(vec![Module { - name: "events".into(), - functions: vec![], - structs: vec![StructDef { - name: "Event".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - enums: vec![], - callbacks: vec![CallbackDef { - name: "on_event".into(), - params: vec![param("event", TypeRef::Record("Event".into()))], - doc: None, - }], - listeners: vec![ListenerDef { - name: "events".into(), - event_callback: "on_event".into(), - doc: None, - }], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - // The native callback typedef carries the (ptr, len) pair + context. - assert!( - dart.contains( - "typedef _NativeCb_weaveffi_events_on_event_fn = Void Function(Pointer, Size, Pointer);" - ), - "missing buffered callback typedef: {dart}" - ); - // The trampoline decodes inside the borrow window, then dispatches. - assert!( - dart.contains("(Pointer eventPtr, int eventLen, Pointer context) {"), - "missing trampoline slots: {dart}" - ); - assert!( - dart.contains("final eventData = _copyNativeBytes(eventPtr, eventLen);") - && dart.contains("final eventValue = _unpackEvent(eventReader);") - && dart.contains("callback(eventValue);"), - "trampoline must decode before invoking the user callback: {dart}" - ); - // Borrowed: never freed by the consumer. - assert!( - !dart.contains("_weaveffiFreeBytes(eventPtr"), - "borrowed callback argument must not be freed: {dart}" - ); - // Register/unregister plumbing is unchanged. - assert!( - dart.contains("int registerEvents(void Function(Event event) callback) {") - && dart.contains("void unregisterEvents(int id) {"), - "missing listener wrappers: {dart}" - ); -} - -#[test] -fn strip_module_prefix_can_be_disabled() { - let api = store_api(); - let model = BindingModel::build(&api, "weaveffi"); - let config = DartConfig { - prefix: Some("weaveffi".into()), - input_basename: Some("kv.yml".into()), - strip_module_prefix: false, - ..DartConfig::default() - }; - let dart = DartGenerator.render_dart_source(&api, &model, &config); - assert!( - dart.contains("int kvInspect(Store store) {"), - "disabled stripping must keep the module prefix: {dart}" - ); - // Interface members are namespaced by their class, never prefixed. - assert!( - dart.contains("factory Store.open(String path) {"), - "interface members must not gain a module prefix: {dart}" - ); -} - -/// IDL names that are Dart reserved words gain a trailing underscore at -/// every user-chosen identifier position: parameters, locals derived from -/// them, record fields (declaration, constructor, pack, and unpack), and -/// enum variant names. Non-reserved names pass through untouched. -#[test] -fn reserved_word_identifiers_are_escaped() { - use weaveffi_ir::ir::{CallbackDef, ListenerDef}; - let api = make_api(vec![Module { - name: "kw".into(), - functions: vec![func( - "load", - vec![ - param("class", TypeRef::StringUtf8), - param("in", TypeRef::I32), - param("normal", TypeRef::Bool), - ], - Some(TypeRef::I32), - )], - structs: vec![StructDef { - name: "Config".into(), - doc: None, - fields: vec![ - field("class", TypeRef::StringUtf8), - field("default", TypeRef::Optional(Box::new(TypeRef::I32))), - field("normal", TypeRef::Bool), - ], - }], - enums: vec![EnumDef { - name: "Mode".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "New".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Existing".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![CallbackDef { - name: "on_kw".into(), - params: vec![param("switch", TypeRef::I32)], - doc: None, - }], - listeners: vec![ListenerDef { - name: "kw_events".into(), - event_callback: "on_kw".into(), - doc: None, - }], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let dart = render_dart_module(&api, "weaveffi", "weaveffi.yml"); - - // Parameters escape in the wrapper signature; the derived staging local - // keeps the escaped base. - assert!( - dart.contains("int load(String class_, int in_, bool normal) {"), - "reserved parameter names must be escaped: {dart}" - ); - assert!( - dart.contains("final class_Ptr = class_.toNativeUtf8();"), - "locals derived from an escaped name must reuse it: {dart}" - ); - // Record fields escape at every position: declaration, constructor, - // pack expression, and unpack key. - assert!( - dart.contains("final String class_;") && dart.contains("final int? default_;"), - "reserved field names must be escaped: {dart}" - ); - assert!( - dart.contains("Config({required this.class_, this.default_, required this.normal});"), - "constructor params must use the escaped names: {dart}" - ); - assert!( - dart.contains("w.writeString(v.class_);"), - "pack helper must read the escaped field: {dart}" - ); - assert!( - dart.contains("class_: r.readString(),"), - "unpack helper must name the escaped field: {dart}" - ); - // Enum variant names escape too (`New` lower-camels to the reserved - // `new`). - assert!( - dart.contains("new_(0),") && dart.contains("existing(1),"), - "reserved variant names must be escaped: {dart}" - ); - // Callback trampoline slots and the user-facing callback signature - // escape reserved parameter names. - assert!( - dart.contains("void Function(int switch_) callback"), - "callback signature must escape reserved names: {dart}" - ); - assert!( - !dart.contains(" switch,") && !dart.contains("(int switch)"), - "no bare reserved word may survive as an identifier: {dart}" - ); -} - -/// Domain error mapping covers exactly the declared (positive) codes: -/// every other code, in particular the reserved negative runtime range -/// (`-1` generic, `-2` panic, `-3` marshalling), falls through to the -/// generic `WeaveFFIException` on throwing paths, and non-throwing paths -/// trap through `_checkError`, which always throws the generic exception. -#[test] -fn runtime_error_codes_fall_through_to_generic() { - let dart = render_dart_module(&store_api(), "weaveffi", "kv.yml"); - - // The mapper switches only on the declared positive codes. - let mapper_start = dart - .find("WeaveFFIException _mapKvException(") - .expect("domain mapper"); - let mapper = &dart[mapper_start..]; - let mapper = &mapper[..mapper.find("\n}").expect("mapper end")]; - assert!( - mapper.contains("case 1001:") && mapper.contains("case 1004:"), - "mapper must cover each declared code: {mapper}" - ); - assert!( - !mapper.contains("case -"), - "no negative code may gain a typed case: {mapper}" - ); - assert!( - mapper.contains("default:\n return WeaveFFIException(code, message);"), - "undeclared codes must fall through to the generic exception: {mapper}" - ); - - // Non-throwing paths trap through the generic check, which throws the - // branded exception for any non-zero code (runtime codes included). - assert!( - dart.contains("throw WeaveFFIException(code, msg);"), - "generic check must throw the branded exception: {dart}" - ); - // A non-throwing async completion also builds the generic exception - // rather than the domain mapper. - let count_body = &dart[dart.find("int count()").expect("count body")..]; - assert!( - count_body.contains("_checkError(err);"), - "non-throwing member must trap generically: {dart}" - ); -} diff --git a/crates/weaveffi-gen-dart/src/types.rs b/crates/weaveffi-gen-dart/src/types.rs index adccce75..7cb2a6f6 100644 --- a/crates/weaveffi-gen-dart/src/types.rs +++ b/crates/weaveffi-gen-dart/src/types.rs @@ -5,9 +5,9 @@ use heck::{ToLowerCamelCase, ToUpperCamelCase}; use weaveffi_core::lang; use weaveffi_core::model::ParamBinding; +use weaveffi_core::model::Ty; use weaveffi_core::plan::{self, ArgPass, RetPass}; use weaveffi_core::utils::{local_type_name, wrapper_name}; -use weaveffi_ir::ir::TypeRef; /// The Dart spelling of an IDL value identifier (parameter, field, or the /// base of a derived local): lowerCamelCase via heck, then keyword-escaped @@ -30,36 +30,33 @@ pub(crate) fn dart_wrapper_fn_name( ) } -/// The idiomatic Dart type a [`TypeRef`] surfaces as. -pub(crate) fn dart_type(ty: &TypeRef) -> String { +/// The idiomatic Dart type a [`Ty`] surfaces as. +pub(crate) fn dart_type(ty: &Ty) -> String { match ty { - TypeRef::I8 - | TypeRef::I16 - | TypeRef::I32 - | TypeRef::I64 - | TypeRef::U8 - | TypeRef::U16 - | TypeRef::U32 - | TypeRef::U64 - | TypeRef::Handle => "int".into(), - TypeRef::F32 | TypeRef::F64 => "double".into(), - TypeRef::Bool => "bool".into(), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "String".into(), - TypeRef::Bytes | TypeRef::BorrowedBytes => "List".into(), + Ty::I8 + | Ty::I16 + | Ty::I32 + | Ty::I64 + | Ty::U8 + | Ty::U16 + | Ty::U32 + | Ty::U64 + | Ty::Handle => "int".into(), + Ty::F32 | Ty::F64 => "double".into(), + Ty::Bool => "bool".into(), + Ty::StringUtf8 | Ty::BorrowedStr => "String".into(), + Ty::Bytes | Ty::BorrowedBytes => "List".into(), // Records, rich enums, C-style enums, typed handles, and interfaces // all surface as bare local Dart classes. A cross-module reference // (resolved to e.g. `kv.Store`) must still name the local `Store` // class, not the qualified IR name. - TypeRef::TypedHandle(n) - | TypeRef::Enum(n) - | TypeRef::Record(n) - | TypeRef::RichEnum(n) - | TypeRef::Interface(n) => local_type_name(n).to_upper_camel_case(), - TypeRef::Named(_) => unreachable!("unresolved type reference"), - TypeRef::Optional(inner) => format!("{}?", dart_type(inner)), - TypeRef::List(inner) => format!("List<{}>", dart_type(inner)), - TypeRef::Iterator(inner) => format!("Iterable<{}>", dart_type(inner)), - TypeRef::Map(k, v) => format!("Map<{}, {}>", dart_type(k), dart_type(v)), + Ty::TypedHandle(n) | Ty::Enum(n) | Ty::Record(n) | Ty::RichEnum(n) | Ty::Interface(n) => { + local_type_name(n).to_upper_camel_case() + } + 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)), } } @@ -70,19 +67,19 @@ pub(crate) fn dart_class(name: &str) -> String { /// dart:ffi (native, dart) types of a leaf scalar passed by value. `Bool` is /// one byte, matching the producer's C `bool`, so by-value slots stay honest. -pub(crate) fn scalar_ffi(ty: &TypeRef) -> (&'static str, &'static str) { +pub(crate) fn scalar_ffi(ty: &Ty) -> (&'static str, &'static str) { match ty { - TypeRef::I8 => ("Int8", "int"), - TypeRef::I16 => ("Int16", "int"), - TypeRef::U8 => ("Uint8", "int"), - TypeRef::U16 => ("Uint16", "int"), - TypeRef::U32 => ("Uint32", "int"), - TypeRef::U64 => ("Uint64", "int"), - TypeRef::I32 | TypeRef::Enum(_) => ("Int32", "int"), - TypeRef::Bool => ("Bool", "bool"), - TypeRef::I64 | TypeRef::Handle => ("Int64", "int"), - TypeRef::F32 => ("Float", "double"), - TypeRef::F64 => ("Double", "double"), + Ty::I8 => ("Int8", "int"), + Ty::I16 => ("Int16", "int"), + Ty::U8 => ("Uint8", "int"), + Ty::U16 => ("Uint16", "int"), + Ty::U32 => ("Uint32", "int"), + Ty::U64 => ("Uint64", "int"), + Ty::I32 | Ty::Enum(_) => ("Int32", "int"), + Ty::Bool => ("Bool", "bool"), + Ty::I64 | Ty::Handle => ("Int64", "int"), + Ty::F32 => ("Float", "double"), + Ty::F64 => ("Double", "double"), _ => ("Int64", "int"), } } @@ -106,7 +103,7 @@ pub(crate) fn input_slots(p: &ParamBinding) -> Vec<(String, String)> { ArgPass::Object { .. } => vec![ptr("Pointer")], ArgPass::Direct { .. } => match &p.ty { // A typed handle's direct slot is an opaque pointer. - TypeRef::TypedHandle(_) => vec![ptr("Pointer")], + Ty::TypedHandle(_) => vec![ptr("Pointer")], ty => { let (n, d) = scalar_ffi(ty); vec![(n.into(), d.into())] @@ -119,7 +116,7 @@ pub(crate) fn input_slots(p: &ParamBinding) -> Vec<(String, String)> { /// return's [`RetPass`] contract. Buffered and bytes returns come back as a /// producer-allocated `Pointer`; strings as `Pointer`; /// interfaces and typed handles as opaque pointers. -pub(crate) fn return_ffi(ty: &TypeRef) -> (String, String) { +pub(crate) fn return_ffi(ty: &Ty) -> (String, String) { let ptr = |s: &str| (s.to_string(), s.to_string()); // Module and prefix only shape an object return's destroy symbol, which // the FFI typedef never names; empty context is fine here. @@ -129,7 +126,7 @@ pub(crate) fn return_ffi(ty: &TypeRef) -> (String, String) { RetPass::Object { .. } => ptr("Pointer"), RetPass::Void | RetPass::Direct => match ty { // A typed handle's direct slot is an opaque pointer. - TypeRef::TypedHandle(_) => ptr("Pointer"), + Ty::TypedHandle(_) => ptr("Pointer"), ty => { let (n, d) = scalar_ffi(ty); (n.into(), d.into()) @@ -140,7 +137,7 @@ pub(crate) fn return_ffi(ty: &TypeRef) -> (String, String) { /// The trailing FFI typedef slots (native, dart) a return type contributes: /// bytes and every buffered return add a single `size_t* out_len`. -pub(crate) fn return_out_slots(ty: &TypeRef) -> Vec<(String, String)> { +pub(crate) fn return_out_slots(ty: &Ty) -> Vec<(String, String)> { if returns_buffer(ty) { vec![("Pointer".into(), "Pointer".into())] } else { @@ -150,7 +147,7 @@ pub(crate) fn return_out_slots(ty: &TypeRef) -> Vec<(String, String)> { /// Whether a return owes the caller a decode from a producer-allocated /// `(ptr, out_len)` buffer (a bytes return or any buffered value). -pub(crate) fn returns_buffer(ty: &TypeRef) -> bool { +pub(crate) fn returns_buffer(ty: &Ty) -> bool { matches!( plan::ret_pass(Some(ty), "", ""), RetPass::Buffer | RetPass::Bytes diff --git a/crates/weaveffi-gen-dotnet/Cargo.toml b/crates/weaveffi-gen-dotnet/Cargo.toml index 7a330a15..570cb090 100644 --- a/crates/weaveffi-gen-dotnet/Cargo.toml +++ b/crates/weaveffi-gen-dotnet/Cargo.toml @@ -11,13 +11,9 @@ categories = ["development-tools::ffi"] [dependencies] weaveffi-core = { path = "../weaveffi-core", version = "0.21.0" } -weaveffi-ir = { path = "../weaveffi-ir", version = "0.21.0" } camino = { workspace = true } heck = { workspace = true } serde = { workspace = true } -[dev-dependencies] -tempfile = { workspace = true } - [lints] workspace = true diff --git a/crates/weaveffi-gen-dotnet/src/calls.rs b/crates/weaveffi-gen-dotnet/src/calls.rs index af51b02c..f34f243c 100644 --- a/crates/weaveffi-gen-dotnet/src/calls.rs +++ b/crates/weaveffi-gen-dotnet/src/calls.rs @@ -6,13 +6,13 @@ use heck::ToUpperCamelCase; use weaveffi_core::abi; use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ CallShape, ErrorBinding, FnBinding, IteratorBinding, ListenerBinding, ModuleBinding, ParamBinding, }; use weaveffi_core::plan::{self, ArgPass, ElemFree, ErrorStrategy}; use weaveffi_core::utils::{local_type_name, wrapper_name}; -use weaveffi_ir::ir::TypeRef; use crate::codec::{emit_buffer_decode, emit_buffer_write}; use crate::docs::{writer_doc, writer_fn_doc}; @@ -180,10 +180,10 @@ pub(crate) fn build_call_args(params: &[ParamBinding]) -> String { // Direct slots pass by value; only the C# spelling of the // value expression depends on the surface type. ArgPass::Direct { .. } => match &p.ty { - TypeRef::Bool => vec![format!("(byte)({name} ? 1 : 0)")], - TypeRef::Enum(_) => vec![format!("(int){name}")], + Ty::Bool => vec![format!("(byte)({name} ? 1 : 0)")], + Ty::Enum(_) => vec![format!("(int){name}")], // A typed handle passes its raw pointer token by value. - TypeRef::TypedHandle(_) => vec![format!("{name}.Raw")], + Ty::TypedHandle(_) => vec![format!("{name}.Raw")], _ => vec![name], }, } @@ -217,9 +217,10 @@ pub(crate) fn render_pinvoke_call_and_return( // Bytes and buffered returns deliver their length through the trailing // `size_t* out_len` slot. - let has_out_len = f.ret.as_ref().is_some_and(|r| { - matches!(r, TypeRef::Bytes | TypeRef::BorrowedBytes) || abi::is_buffered(r) - }); + let has_out_len = f + .ret + .as_ref() + .is_some_and(|r| matches!(r, Ty::Bytes | Ty::BorrowedBytes) || r.is_buffered()); let mut w = CodeWriter::four_space().with_depth(indent.len() / 4); if f.ret.is_some() { @@ -252,45 +253,46 @@ pub(crate) fn render_pinvoke_call_and_return( /// Emit the statements converting the raw `result` slot (plus `outLen` for /// bytes and buffered returns) into the returned C# value, releasing /// producer-owned memory along the way. -pub(crate) fn render_return_conversion(out: &mut String, ty: &TypeRef, indent: &str) { +pub(crate) fn render_return_conversion(out: &mut String, ty: &Ty, indent: &str) { let mut w = CodeWriter::four_space().with_depth(indent.len() / 4); // A buffered return is a producer-allocated value buffer: copy the bytes, // release them with `weaveffi_free_bytes`, then decode the copy. - if abi::is_buffered(ty) { + if ty.is_buffered() { w.line("var resultBuf = new byte[(int)outLen];"); w.line( "if (result != IntPtr.Zero && (int)outLen > 0) Marshal.Copy(result, resultBuf, 0, (int)outLen);", ); w.line("NativeMethods.weaveffi_free_bytes(result, outLen);"); - emit_buffer_decode(&mut w, ty, "value", "resultBuf"); - w.line("return value;"); + // Named so it cannot shadow a user parameter (`value` is a common one). + emit_buffer_decode(&mut w, ty, "decoded", "resultBuf"); + w.line("return decoded;"); out.push_str(&w.finish()); return; } match ty { - TypeRef::Bool => { + Ty::Bool => { w.line("return result != 0;"); } - TypeRef::StringUtf8 | TypeRef::BorrowedStr => { + Ty::StringUtf8 | Ty::BorrowedStr => { w.line("var str = Marshal.PtrToStringUTF8(result);"); w.line("NativeMethods.weaveffi_free_string(result);"); w.line("return str ?? \"\";"); } - TypeRef::Enum(name) => { + Ty::Enum(name) => { let cn = local_type_name(name); w.line(format!("return ({cn})result;")); } - TypeRef::TypedHandle(name) => { + Ty::TypedHandle(name) => { let cn = typed_handle_cs(name); w.line(format!("return new {cn}(result);")); } // An interface return transfers ownership (`ReturnFree::OwnedObject`): // wrap the pointer in a new instance whose Dispose() releases it. - TypeRef::Interface(name) => { + Ty::Interface(name) => { let cn = local_type_name(name); w.line(format!("return new {cn}(result);")); } - TypeRef::Bytes | TypeRef::BorrowedBytes => { + Ty::Bytes | Ty::BorrowedBytes => { w.line("if (result == IntPtr.Zero) return Array.Empty();"); w.line("var arr = new byte[(int)outLen];"); w.line("Marshal.Copy(result, arr, 0, (int)outLen);"); @@ -298,8 +300,8 @@ pub(crate) fn render_return_conversion(out: &mut String, ty: &TypeRef, indent: & w.line("return arr;"); } // Only `Interface?` reaches here: a nullable owned object pointer. - TypeRef::Optional(inner) => match inner.as_ref() { - TypeRef::Interface(name) => { + Ty::Optional(inner) => match inner.as_ref() { + Ty::Interface(name) => { let cn = local_type_name(name); w.line(format!( "return result == IntPtr.Zero ? null : new {cn}(result);" @@ -307,8 +309,8 @@ pub(crate) fn render_return_conversion(out: &mut String, ty: &TypeRef, indent: & } _ => unreachable!("non-interface optionals are buffered"), }, - TypeRef::Iterator(_) => unreachable!("iterator functions render via CallShape::Iterator"), - TypeRef::Record(_) | TypeRef::RichEnum(_) | TypeRef::List(_) | TypeRef::Map(_, _) => { + Ty::Iterator(_) => unreachable!("iterator functions render via CallShape::Iterator"), + Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => { unreachable!("buffered return handled above") } _ => { @@ -401,9 +403,9 @@ pub(crate) fn render_wrapper_method( /// classification (`ElemFree::String` via `weaveffi_free_string`, /// `ElemFree::Bytes` via `weaveffi_free_bytes` for both bytes and buffered /// elements). Returns the expression to `yield return`. -pub(crate) fn iterator_item_conversion(out: &mut String, elem: &TypeRef, indent: &str) -> String { +pub(crate) fn iterator_item_conversion(out: &mut String, elem: &Ty, indent: &str) -> String { let mut w = CodeWriter::four_space().with_depth(indent.len() / 4); - if abi::is_buffered(elem) { + if elem.is_buffered() { w.line("var itemBuf = new byte[(int)out_len];"); w.line("if (out_item != IntPtr.Zero && (int)out_len > 0) Marshal.Copy(out_item, itemBuf, 0, (int)out_len);"); w.line("NativeMethods.weaveffi_free_bytes(out_item, out_len);"); @@ -426,25 +428,25 @@ pub(crate) fn iterator_item_conversion(out: &mut String, elem: &TypeRef, indent: "item".into() } ElemFree::None => match elem { - TypeRef::I8 - | TypeRef::I16 - | TypeRef::I32 - | TypeRef::U8 - | TypeRef::U16 - | TypeRef::U32 - | TypeRef::I64 - | TypeRef::U64 - | TypeRef::F32 - | TypeRef::F64 - | TypeRef::Handle => "out_item".into(), - TypeRef::Bool => "out_item != 0".into(), - TypeRef::Enum(name) => format!("({})out_item", local_type_name(name)), - TypeRef::TypedHandle(name) => { + Ty::I8 + | Ty::I16 + | Ty::I32 + | Ty::U8 + | Ty::U16 + | Ty::U32 + | Ty::I64 + | Ty::U64 + | Ty::F32 + | Ty::F64 + | Ty::Handle => "out_item".into(), + Ty::Bool => "out_item != 0".into(), + Ty::Enum(name) => format!("({})out_item", local_type_name(name)), + Ty::TypedHandle(name) => { format!("new {}(out_item)", typed_handle_cs(name)) } // The consumer owns each yielded wrapper; Dispose() destroys it // (owned-object elements are adopted rather than freed eagerly). - TypeRef::Interface(name) => { + Ty::Interface(name) => { format!("new {}(out_item)", local_type_name(name)) } other => unreachable!("unsupported iterator element type {other:?}"), @@ -608,13 +610,13 @@ pub(crate) fn render_iterator_wrapper_method( /// True when an async result crosses the completion callback as an owned /// `ptr` + `len` pair: bytes and every buffered type (records, rich enums, /// lists, maps, and non-interface optionals). -pub(crate) fn async_result_is_ptr_len(ty: &TypeRef) -> bool { - matches!(ty, TypeRef::Bytes | TypeRef::BorrowedBytes) || abi::is_buffered(ty) +pub(crate) fn async_result_is_ptr_len(ty: &Ty) -> bool { + matches!(ty, Ty::Bytes | Ty::BorrowedBytes) || ty.is_buffered() } /// The completion lambda's formal parameter list for one async return type, /// matching the delegate declared by the P/Invoke layer. -pub(crate) fn async_cb_lambda_params(ret: &Option) -> &'static str { +pub(crate) fn async_cb_lambda_params(ret: &Option) -> &'static str { match ret { None => "(context, err)", Some(ty) if async_result_is_ptr_len(ty) => "(context, err, result, resultLen)", @@ -798,10 +800,10 @@ pub(crate) fn render_async_wrapper_method( /// here and then released through the runtime free symbols. Owned-object /// results (interfaces, typed handles) transfer ownership instead: the /// wrapper adopts the pointer. -pub(crate) fn render_async_set_result(out: &mut String, ret: &Option, indent: &str) { +pub(crate) fn render_async_set_result(out: &mut String, ret: &Option, indent: &str) { let mut w = CodeWriter::four_space().with_depth(indent.len() / 4); if let Some(ty) = ret { - if abi::is_buffered(ty) { + if ty.is_buffered() { w.line("var resultBuf = new byte[(int)resultLen];"); w.line( "if (result != IntPtr.Zero && (int)resultLen > 0) Marshal.Copy(result, resultBuf, 0, (int)resultLen);", @@ -809,8 +811,8 @@ pub(crate) fn render_async_set_result(out: &mut String, ret: &Option, i w.line( "if (result != IntPtr.Zero) NativeMethods.weaveffi_free_bytes(result, resultLen);", ); - emit_buffer_decode(&mut w, ty, "value", "resultBuf"); - w.line("tcs.SetResult(value);"); + emit_buffer_decode(&mut w, ty, "decoded", "resultBuf"); + w.line("tcs.SetResult(decoded);"); out.push_str(&w.finish()); return; } @@ -819,27 +821,27 @@ pub(crate) fn render_async_set_result(out: &mut String, ret: &Option, i None => { w.line("tcs.SetResult(true);"); } - Some(TypeRef::Bool) => { + Some(Ty::Bool) => { w.line("tcs.SetResult(result != 0);"); } - Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => { + Some(Ty::StringUtf8 | Ty::BorrowedStr) => { w.line("var str = Marshal.PtrToStringUTF8(result) ?? \"\";"); w.line("if (result != IntPtr.Zero) NativeMethods.weaveffi_free_string(result);"); w.line("tcs.SetResult(str);"); } - Some(TypeRef::Enum(name)) => { + Some(Ty::Enum(name)) => { let cn = local_type_name(name); w.line(format!("tcs.SetResult(({cn})result);")); } - Some(TypeRef::TypedHandle(name)) => { + Some(Ty::TypedHandle(name)) => { let cn = typed_handle_cs(name); w.line(format!("tcs.SetResult(new {cn}(result));")); } - Some(TypeRef::Interface(name)) => { + Some(Ty::Interface(name)) => { let cn = local_type_name(name); w.line(format!("tcs.SetResult(new {cn}(result));")); } - Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => { + Some(Ty::Bytes | Ty::BorrowedBytes) => { w.line("var arr = new byte[(int)resultLen];"); w.line( "if (result != IntPtr.Zero && (int)resultLen > 0) Marshal.Copy(result, arr, 0, (int)resultLen);", @@ -850,8 +852,8 @@ pub(crate) fn render_async_set_result(out: &mut String, ret: &Option, i w.line("tcs.SetResult(arr);"); } // Only `Interface?` reaches here: a nullable owned object pointer. - Some(TypeRef::Optional(inner)) => match inner.as_ref() { - TypeRef::Interface(name) => { + Some(Ty::Optional(inner)) => match inner.as_ref() { + Ty::Interface(name) => { let cn = local_type_name(name); w.line(format!( "tcs.SetResult(result == IntPtr.Zero ? null : new {cn}(result));" @@ -881,7 +883,7 @@ pub(crate) fn render_cb_arg( let slots = abi::lower_param(&p.name, &p.ty, "", false); let n0 = safe_cs_name(&slots[0].name); let mut w = CodeWriter::four_space().with_depth(indent.len() / 4); - if abi::is_buffered(&p.ty) { + if p.ty.is_buffered() { let len = safe_cs_name(&slots[1].name); let arg = format!("arg{idx}"); w.line(format!("var {arg}Buf = new byte[(int){len}];")); @@ -893,23 +895,23 @@ pub(crate) fn render_cb_arg( return arg; } let expr = match &p.ty { - TypeRef::I8 - | TypeRef::I16 - | TypeRef::I32 - | TypeRef::U8 - | TypeRef::U16 - | TypeRef::U32 - | TypeRef::I64 - | TypeRef::U64 - | TypeRef::F32 - | TypeRef::F64 => n0, - TypeRef::Handle => n0, - TypeRef::Bool => format!("{n0} != 0"), - TypeRef::Enum(name) => format!("({}){n0}", local_type_name(name)), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => { + Ty::I8 + | Ty::I16 + | Ty::I32 + | Ty::U8 + | Ty::U16 + | Ty::U32 + | Ty::I64 + | Ty::U64 + | Ty::F32 + | Ty::F64 => n0, + Ty::Handle => n0, + Ty::Bool => format!("{n0} != 0"), + Ty::Enum(name) => format!("({}){n0}", local_type_name(name)), + Ty::StringUtf8 | Ty::BorrowedStr => { format!("Marshal.PtrToStringUTF8({n0}) ?? \"\"") } - TypeRef::Bytes | TypeRef::BorrowedBytes => { + Ty::Bytes | Ty::BorrowedBytes => { let len = safe_cs_name(&slots[1].name); let arg = format!("arg{idx}"); w.line(format!("var {arg} = new byte[(int){len}];")); @@ -918,27 +920,26 @@ pub(crate) fn render_cb_arg( )); arg } - TypeRef::TypedHandle(name) => { + Ty::TypedHandle(name) => { format!("new {}({n0})", typed_handle_cs(name)) } // Borrowed for the duration of the callback; the consumer must not // Dispose() the wrapper. - TypeRef::Interface(name) => { + Ty::Interface(name) => { format!("new {}({n0})", local_type_name(name)) } // Only `Interface?` reaches here: every other optional is buffered. - TypeRef::Optional(inner) => match inner.as_ref() { - TypeRef::Interface(name) => { + Ty::Optional(inner) => match inner.as_ref() { + Ty::Interface(name) => { let cn = local_type_name(name); format!("{n0} == IntPtr.Zero ? null : new {cn}({n0})") } _ => unreachable!("non-interface optionals are buffered"), }, - TypeRef::Record(_) | TypeRef::RichEnum(_) | TypeRef::List(_) | TypeRef::Map(_, _) => { + Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => { unreachable!("buffered callback parameter handled above") } - TypeRef::Iterator(_) => unreachable!("iterator not valid as callback parameter"), - TypeRef::Named(_) => unreachable!("unresolved type reference"), + Ty::Iterator(_) => unreachable!("iterator not valid as callback parameter"), }; out.push_str(&w.finish()); expr diff --git a/crates/weaveffi-gen-dotnet/src/codec.rs b/crates/weaveffi-gen-dotnet/src/codec.rs index c0c36bc8..fa1c2de2 100644 --- a/crates/weaveffi-gen-dotnet/src/codec.rs +++ b/crates/weaveffi-gen-dotnet/src/codec.rs @@ -5,14 +5,14 @@ //! Every dispatch here goes through [`wire::classify`], so this module never //! re-derives the wire folds (handles as `u64` tokens, borrowed views as //! their owned forms, records and rich enums as one user-codec shape). The -//! only peek back at the [`TypeRef`] is for `handle`, whose C# surface is +//! only peek back at the [`Ty`] is for `handle`, whose C# surface is //! a `{T}Handle` wrapper struct even though the wire shape is one `u64` //! token. use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; +use weaveffi_core::model::{Prim, WireType}; use weaveffi_core::utils::local_type_name; -use weaveffi_core::wire::{self, WireType}; -use weaveffi_ir::ir::TypeRef; use crate::types::{cs_type, is_cs_value_type, typed_handle_cs}; @@ -22,49 +22,49 @@ use crate::types::{cs_type, is_cs_value_type, typed_handle_cs}; /// `depth` uniquifies loop locals so nested lists and maps never collide. pub(crate) fn emit_buffer_write( w: &mut CodeWriter, - ty: &TypeRef, + ty: &Ty, expr: &str, writer_var: &str, depth: usize, ) { - match wire::classify(ty) { - WireType::I8 => { + match ty.wire() { + WireType::Prim(Prim::I8) => { w.line(format!("{writer_var}.WriteI8({expr});")); } - WireType::I16 => { + WireType::Prim(Prim::I16) => { w.line(format!("{writer_var}.WriteI16({expr});")); } - WireType::I32 => { + WireType::Prim(Prim::I32) => { w.line(format!("{writer_var}.WriteI32({expr});")); } - WireType::U8 => { + WireType::Prim(Prim::U8) => { w.line(format!("{writer_var}.WriteU8({expr});")); } - WireType::U16 => { + WireType::Prim(Prim::U16) => { w.line(format!("{writer_var}.WriteU16({expr});")); } - WireType::U32 => { + WireType::Prim(Prim::U32) => { w.line(format!("{writer_var}.WriteU32({expr});")); } - WireType::I64 => { + WireType::Prim(Prim::I64) => { w.line(format!("{writer_var}.WriteI64({expr});")); } - WireType::U64 => { + WireType::Prim(Prim::U64) => { w.line(format!("{writer_var}.WriteU64({expr});")); } - WireType::F32 => { + WireType::Prim(Prim::F32) => { w.line(format!("{writer_var}.WriteF32({expr});")); } - WireType::F64 => { + WireType::Prim(Prim::F64) => { w.line(format!("{writer_var}.WriteF64({expr});")); } - WireType::Bool => { + WireType::Prim(Prim::Bool) => { w.line(format!("{writer_var}.WriteBool({expr});")); } // Both handle flavors encode as one u64 token; only the C# surface // differs (a bare ulong versus the `{T}Handle` wrapper struct). - WireType::Handle => { - if matches!(ty, TypeRef::TypedHandle(_)) { + WireType::Handle(_) => { + if matches!(ty, Ty::TypedHandle(_)) { w.line(format!("{writer_var}.WriteU64((ulong)(long){expr}.Raw);")); } else { w.line(format!("{writer_var}.WriteU64({expr});")); @@ -73,10 +73,10 @@ pub(crate) fn emit_buffer_write( WireType::Enum(_) => { w.line(format!("{writer_var}.WriteI32((int){expr});")); } - WireType::String => { + WireType::Prim(Prim::String) => { w.line(format!("{writer_var}.WriteString({expr});")); } - WireType::Bytes => { + WireType::Prim(Prim::Bytes) => { w.line(format!("{writer_var}.WriteBytes({expr});")); } WireType::User(_) => { @@ -123,49 +123,49 @@ pub(crate) fn emit_buffer_write( /// [`emit_buffer_write`]. `depth` uniquifies loop counters across nesting. pub(crate) fn emit_buffer_read( w: &mut CodeWriter, - ty: &TypeRef, + ty: &Ty, var: &str, reader_var: &str, depth: usize, ) { - match wire::classify(ty) { - WireType::I8 => { + match ty.wire() { + WireType::Prim(Prim::I8) => { w.line(format!("var {var} = {reader_var}.ReadI8();")); } - WireType::I16 => { + WireType::Prim(Prim::I16) => { w.line(format!("var {var} = {reader_var}.ReadI16();")); } - WireType::I32 => { + WireType::Prim(Prim::I32) => { w.line(format!("var {var} = {reader_var}.ReadI32();")); } - WireType::U8 => { + WireType::Prim(Prim::U8) => { w.line(format!("var {var} = {reader_var}.ReadU8();")); } - WireType::U16 => { + WireType::Prim(Prim::U16) => { w.line(format!("var {var} = {reader_var}.ReadU16();")); } - WireType::U32 => { + WireType::Prim(Prim::U32) => { w.line(format!("var {var} = {reader_var}.ReadU32();")); } - WireType::I64 => { + WireType::Prim(Prim::I64) => { w.line(format!("var {var} = {reader_var}.ReadI64();")); } - WireType::U64 => { + WireType::Prim(Prim::U64) => { w.line(format!("var {var} = {reader_var}.ReadU64();")); } - WireType::F32 => { + WireType::Prim(Prim::F32) => { w.line(format!("var {var} = {reader_var}.ReadF32();")); } - WireType::F64 => { + WireType::Prim(Prim::F64) => { w.line(format!("var {var} = {reader_var}.ReadF64();")); } - WireType::Bool => { + WireType::Prim(Prim::Bool) => { w.line(format!("var {var} = {reader_var}.ReadBool();")); } // The u64 token decodes back into the C# surface: a bare ulong for // `handle`, the `{T}Handle` wrapper for `handle`. - WireType::Handle => { - if let TypeRef::TypedHandle(name) = ty { + WireType::Handle(_) => { + if let Ty::TypedHandle(name) = ty { let cn = typed_handle_cs(name); w.line(format!( "var {var} = new {cn}((IntPtr)(long){reader_var}.ReadU64());" @@ -178,10 +178,10 @@ pub(crate) fn emit_buffer_read( let cn = local_type_name(name); w.line(format!("var {var} = ({cn}){reader_var}.ReadI32();")); } - WireType::String => { + WireType::Prim(Prim::String) => { w.line(format!("var {var} = {reader_var}.ReadString();")); } - WireType::Bytes => { + WireType::Prim(Prim::Bytes) => { w.line(format!("var {var} = {reader_var}.ReadBytes();")); } WireType::User(name) => { @@ -199,9 +199,11 @@ pub(crate) fn emit_buffer_read( } WireType::List(inner) => { let i = format!("i{depth}"); - let elem = cs_type(inner); w.line(format!("var {var}Count = {reader_var}.ReadLen();")); - w.line(format!("var {var} = new {elem}[{var}Count];")); + w.line(format!( + "var {var} = {};", + cs_new_array(&cs_type(inner), &format!("{var}Count")) + )); w.line(format!("for (int {i} = 0; {i} < {var}Count; {i}++)")); w.block("{", "}", |w| { emit_buffer_read(w, inner, &format!("{var}Item"), reader_var, depth + 1); @@ -229,7 +231,18 @@ pub(crate) fn emit_buffer_read( /// Emit the statements decoding a consumer-side copy of a value buffer /// (`byte[]` local named `buf`) into a local named `var` of type `ty`, /// validating that the buffer is fully consumed. -pub(crate) fn emit_buffer_decode(w: &mut CodeWriter, ty: &TypeRef, var: &str, buf: &str) { +/// `new T[len]` for an array whose element type is `elem`. +/// +/// C# puts the outermost rank first, so an array of `int[]` elements is +/// spelled `new int[len][]`, not `new int[][len]`: any trailing `[]` ranks on +/// the element type move after the length. +fn cs_new_array(elem: &str, len: &str) -> String { + let base = elem.trim_end_matches("[]"); + let ranks = &elem[base.len()..]; + format!("new {base}[{len}]{ranks}") +} + +pub(crate) fn emit_buffer_decode(w: &mut CodeWriter, ty: &Ty, var: &str, buf: &str) { w.line(format!( "var {var}Reader = new WeaveFFIBufferReader({buf});" )); diff --git a/crates/weaveffi-gen-dotnet/src/entities.rs b/crates/weaveffi-gen-dotnet/src/entities.rs index c6036095..bcc01151 100644 --- a/crates/weaveffi-gen-dotnet/src/entities.rs +++ b/crates/weaveffi-gen-dotnet/src/entities.rs @@ -3,12 +3,12 @@ use heck::{ToLowerCamelCase, ToUpperCamelCase}; use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ BindingModel, CallShape, EnumBinding, EnumVariantBinding, ErrorBinding, FieldBinding, FnBinding, InterfaceBinding, StructBinding, }; use weaveffi_core::utils::local_type_name; -use weaveffi_ir::ir::TypeRef; use crate::calls::{ build_call_args, param_needs_marshal, render_marshal_cleanup, render_marshal_setup, @@ -23,15 +23,15 @@ use crate::types::{camel_fn, cs_type, safe_cs_name, typed_handle_cs}; /// parameters, and error payload fields), so one `{T}Handle` wrapper struct /// is emitted per referent. The `BTreeSet` keeps emission order stable. pub(crate) fn collect_typed_handles(model: &BindingModel) -> std::collections::BTreeSet { - fn visit(ty: &TypeRef, acc: &mut std::collections::BTreeSet) { + fn visit(ty: &Ty, acc: &mut std::collections::BTreeSet) { match ty { - TypeRef::TypedHandle(name) => { + Ty::TypedHandle(name) => { acc.insert(local_type_name(name).to_string()); } - TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => { + Ty::Optional(inner) | Ty::List(inner) | Ty::Iterator(inner) => { visit(inner, acc); } - TypeRef::Map(k, v) => { + Ty::Map(k, v) => { visit(k, acc); visit(v, acc); } diff --git a/crates/weaveffi-gen-dotnet/src/lib.rs b/crates/weaveffi-gen-dotnet/src/lib.rs index 6033551e..4b3df329 100644 --- a/crates/weaveffi-gen-dotnet/src/lib.rs +++ b/crates/weaveffi-gen-dotnet/src/lib.rs @@ -22,8 +22,6 @@ mod entities; mod package; mod pinvoke; mod runtime; -#[cfg(test)] -mod tests; mod types; use camino::Utf8Path; @@ -233,8 +231,6 @@ impl LanguageBackend for DotnetGenerator { } } -weaveffi_core::impl_generator_via_backend!(DotnetGenerator); - /// Render the complete generated C# source file: the prelude, usings, the /// shared runtime types, every module's entities, the `NativeMethods` extern /// class, and the per-module static wrapper classes. diff --git a/crates/weaveffi-gen-dotnet/src/package.rs b/crates/weaveffi-gen-dotnet/src/package.rs index f8a7262b..139bb23d 100644 --- a/crates/weaveffi-gen-dotnet/src/package.rs +++ b/crates/weaveffi-gen-dotnet/src/package.rs @@ -9,14 +9,14 @@ use weaveffi_core::manifest::xml_escape; use weaveffi_core::package::PackageContext; use weaveffi_core::pkg::{self, ResolvedPackage}; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; -use weaveffi_ir::ir::Api; use crate::DotnetConfig; /// Resolve the NuGet/package identity for the .NET target, applying the /// namespace-as-name fallback when nothing else identifies the package. -pub(crate) fn resolve_dotnet_package(api: &Api, config: &DotnetConfig) -> ResolvedPackage { +pub(crate) fn resolve_dotnet_package(api: &ResolvedApi, config: &DotnetConfig) -> ResolvedPackage { let namespace = config.namespace(); let mut p = pkg::resolve( api, @@ -26,7 +26,10 @@ pub(crate) fn resolve_dotnet_package(api: &Api, config: &DotnetConfig) -> Resolv // The C# namespace doubles as the file basename; when nothing identifies // the package, keep the PascalCase brand as the NuGet id so // `WeaveFFI.csproj` and `` stay consistent. - if api.package.is_none() && config.namespace.is_none() && config.input_basename.is_none() { + if api.package().and_then(|p| p.name.as_deref()).is_none() + && config.namespace.is_none() + && config.input_basename.is_none() + { p.name = namespace.to_string(); } p diff --git a/crates/weaveffi-gen-dotnet/src/pinvoke.rs b/crates/weaveffi-gen-dotnet/src/pinvoke.rs index bfe9ea88..f2d9749b 100644 --- a/crates/weaveffi-gen-dotnet/src/pinvoke.rs +++ b/crates/weaveffi-gen-dotnet/src/pinvoke.rs @@ -3,12 +3,13 @@ //! exactly. use weaveffi_core::abi::{self, AbiParam, CType}; +use weaveffi_core::cabi::ABI_VERSION; use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ BindingModel, CallShape, CallbackBinding, FnBinding, InterfaceBinding, IteratorBinding, ListenerBinding, ParamBinding, }; -use weaveffi_ir::ir::TypeRef; use crate::calls::async_result_is_ptr_len; use crate::types::{cs_out_param, cs_pinvoke_ctype, pinvoke_type, safe_cs_name}; @@ -30,7 +31,7 @@ pub(crate) fn pinvoke_param_list(p: &ParamBinding) -> Vec { /// The extern return type plus any trailing out-params (for example the /// `size_t* out_len` slot of a buffered return) for one IR return type. -pub(crate) fn pinvoke_return_info(ty: &TypeRef) -> (String, Vec) { +pub(crate) fn pinvoke_return_info(ty: &Ty) -> (String, Vec) { let r = abi::lower_return(ty, ""); ( cs_pinvoke_ctype(&r.ret), @@ -43,10 +44,10 @@ pub(crate) fn is_error_slot(slot: &AbiParam) -> bool { matches!(&slot.ty, CType::Ptr { pointee, .. } if matches!(pointee.as_ref(), CType::Error)) } -/// Render the `NativeMethods` static class: the shared runtime imports -/// (`free_string`, `free_bytes`, `error_clear`, `error_free`) followed by -/// every interface, -/// callback, listener, and function extern in declaration order. +/// Render the `NativeMethods` static class: the ABI-revision check in its +/// static constructor, the shared runtime imports (`abi_version`, +/// `free_string`, `free_bytes`, `error_clear`, `error_free`), and then every +/// interface, callback, listener, and function extern in declaration order. pub(crate) fn render_native_methods(out: &mut String, model: &BindingModel) { let mut w = CodeWriter::four_space().with_depth(1); w.line("internal static class NativeMethods"); @@ -54,6 +55,34 @@ pub(crate) fn render_native_methods(out: &mut String, model: &BindingModel) { w.indent(); w.line("private const string LibName = \"weaveffi\";"); w.blank(); + w.line("// The ABI revision these bindings were generated against."); + w.line(format!("internal const uint AbiVersion = {ABI_VERSION};")); + w.blank(); + w.line("// Runs before the first P/Invoke through this class, so a producer built"); + w.line("// for a different ABI revision fails loudly instead of misreading the"); + w.line("// error struct or a value buffer later."); + w.line("static NativeMethods()"); + w.block("{", "}", |w| { + w.line("uint found;"); + w.line("try"); + w.block("{", "}", |w| { + w.line("found = weaveffi_abi_version();"); + }); + w.line("catch (EntryPointNotFoundException e)"); + w.block("{", "}", |w| { + w.line("throw new InvalidOperationException("); + w.line(" $\"the loaded WeaveFFI library predates ABI versioning (these bindings expect ABI revision {AbiVersion})\", e);"); + }); + w.line("if (found != AbiVersion)"); + w.block("{", "}", |w| { + w.line("throw new InvalidOperationException("); + w.line(" $\"WeaveFFI ABI mismatch: these bindings expect revision {AbiVersion} but the loaded library reports revision {found}\");"); + }); + }); + w.blank(); + w.line("[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]"); + w.line("internal static extern uint weaveffi_abi_version();"); + w.blank(); w.line("[DllImport(LibName, CallingConvention = CallingConvention.Cdecl)]"); w.line("internal static extern void weaveffi_free_string(IntPtr ptr);"); w.blank(); @@ -277,7 +306,7 @@ pub(crate) fn render_iterator_pinvoke(out: &mut String, it: &IteratorBinding) { /// The completion delegate's result parameters for one async return type: /// nothing for void, a borrowed `(ptr, len)` pair for bytes and buffered /// values, and a single by-value slot otherwise. -pub(crate) fn async_cb_delegate_result_params(ret: &Option) -> String { +pub(crate) fn async_cb_delegate_result_params(ret: &Option) -> String { match ret { None => String::new(), Some(ty) if async_result_is_ptr_len(ty) => ", IntPtr result, UIntPtr resultLen".into(), diff --git a/crates/weaveffi-gen-dotnet/src/tests.rs b/crates/weaveffi-gen-dotnet/src/tests.rs deleted file mode 100644 index 0d0932c1..00000000 --- a/crates/weaveffi-gen-dotnet/src/tests.rs +++ /dev/null @@ -1,4581 +0,0 @@ -//! Output-content tests for the .NET generator: fixtures mirroring the -//! sample IDLs plus assertions over the rendered C# source and manifests. - -use super::*; -use crate::types::{cs_type, pinvoke_type, safe_cs_name}; -use weaveffi_core::codegen::Generator; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_ir::ir::Api; -use weaveffi_ir::ir::{ - EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField, TypeRef, -}; - -/// Test shim matching the pre-0.5.0 signature: builds the [`BindingModel`] -/// here so the production `render_csharp` stays model-only. -fn render_csharp( - api: &ResolvedApi, - namespace: &str, - strip_module_prefix: bool, - prefix: &str, - input_basename: &str, - filename: &str, -) -> String { - let model = BindingModel::build(api, prefix); - super::render_csharp( - &model, - namespace, - strip_module_prefix, - input_basename, - filename, - ) -} - -#[test] -fn package_emits_runtimes_and_rebinds_libname() { - use camino::Utf8Path; - use weaveffi_core::package::{FileContent, PackageContext}; - use weaveffi_core::platform::{BinarySet, Platform}; - - let api = make_api(vec![simple_module(vec![Function { - name: "ping".into(), - params: vec![], - returns: None, - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }])]); - let model = BindingModel::build(&api, "weaveffi"); - let mut bins = BinarySet::new("calculator"); - bins.insert(Platform::MacosArm64, "/s/darwin-arm64/libcalculator.dylib"); - bins.insert(Platform::WindowsX64, "/s/windows-x64/calculator.dll"); - let ctx = PackageContext { - binaries: &bins, - input_basename: Some("calculator.yml"), - }; - let files = LanguageBackend::package( - &DotnetGenerator, - &api, - &model, - &ctx, - Utf8Path::new("/out"), - &DotnetConfig::default(), - ) - .expect("dotnet supports packaging"); - - // NuGet `runtimes//native/` layout. - assert!(files.iter().any(|f| f - .path - .as_str() - .ends_with("runtimes/osx-arm64/native/libcalculator.dylib"))); - assert!(files.iter().any(|f| f - .path - .as_str() - .ends_with("runtimes/win-x64/native/calculator.dll"))); - // The P/Invoke library name is rebound to the bundled base name. - let cs = files - .iter() - .find(|f| f.path.as_str().ends_with(".cs")) - .expect("C# source present"); - let FileContent::Text(src) = &cs.content else { - panic!("C# source is text"); - }; - assert!( - src.contains("private const string LibName = \"calculator\";"), - "DllImport name not rebound: {src}" - ); - let csproj = files - .iter() - .find(|f| f.path.as_str().ends_with(".csproj")) - .expect("csproj present"); - let FileContent::Text(proj) = &csproj.content else { - panic!("csproj is text"); - }; - assert!( - proj.contains("runtimes/**"), - "native asset item group missing: {proj}" - ); -} - -fn make_api(modules: Vec) -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules, - generators: None, - package: None, - }) -} - -fn simple_module(functions: Vec) -> Module { - Module { - name: "math".into(), - functions, - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - } -} - -#[test] -fn generator_name_is_dotnet() { - assert_eq!(Generator::name(&DotnetGenerator), "dotnet"); -} - -#[test] -fn output_files_lists_all() { - let api = make_api(vec![]); - let out = Utf8Path::new("/tmp/out"); - let files = DotnetGenerator.output_files(&api, out, &DotnetConfig::default()); - assert_eq!( - files, - vec![ - format!("{out}/dotnet/README.md"), - format!("{out}/dotnet/WeaveFFI.cs"), - format!("{out}/dotnet/WeaveFFI.csproj"), - format!("{out}/dotnet/WeaveFFI.nuspec"), - ] - ); -} - -#[test] -fn generate_creates_output_file() { - let api = make_api(vec![simple_module(vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }])]); - - let tmp = std::env::temp_dir().join("weaveffi_test_dotnet_gen_output"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - DotnetGenerator - .generate(&api, out_dir, &DotnetConfig::default()) - .unwrap(); - - let cs = std::fs::read_to_string(tmp.join("dotnet/WeaveFFI.cs")).unwrap(); - assert!(cs.contains("namespace WeaveFFI")); - assert!(cs.contains("DllImport")); - assert!(cs.contains("weaveffi_math_add")); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn listeners_generate_register_unregister() { - use weaveffi_ir::ir::{CallbackDef, ListenerDef}; - let api = make_api(vec![Module { - name: "events".into(), - functions: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![CallbackDef { - name: "OnMessage".into(), - doc: None, - params: vec![Param { - name: "message".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - }], - listeners: vec![ListenerDef { - name: "message_listener".into(), - event_callback: "OnMessage".into(), - doc: None, - }], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - let dir = tempfile::tempdir().unwrap(); - let out = Utf8Path::from_path(dir.path()).unwrap(); - DotnetGenerator - .generate(&api, out, &DotnetConfig::default()) - .unwrap(); - let cs = std::fs::read_to_string(dir.path().join("dotnet/WeaveFFI.cs")).unwrap(); - assert!( - cs.contains("internal delegate void Cb_weaveffi_events_OnMessage_fn"), - "unmanaged delegate type must be declared: {cs}" - ); - assert!( - cs.contains("[UnmanagedFunctionPointer(CallingConvention.Cdecl)]"), - "delegate must use cdecl: {cs}" - ); - assert!( - cs.contains("internal static extern ulong weaveffi_events_register_message_listener"), - "register pinvoke missing: {cs}" - ); - assert!( - cs.contains("public static ulong RegisterMessageListener(Action callback)"), - "register wrapper missing: {cs}" - ); - assert!( - cs.contains("public static void UnregisterMessageListener(ulong id)"), - "unregister wrapper missing: {cs}" - ); - assert!( - cs.contains("_listenerRefs[id] = trampoline;"), - "delegate must be pinned in the registry: {cs}" - ); - assert!( - cs.contains("Marshal.PtrToStringUTF8(message) ?? \"\""), - "string arg must be marshaled: {cs}" - ); -} - -#[test] -fn dotnet_record_is_plain_value_class() { - let api = ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules: vec![Module { - name: "contacts".into(), - functions: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "age".into(), - ty: TypeRef::I32, - doc: None, - }, - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }], - generators: None, - package: None, - }); - let dir = tempfile::tempdir().unwrap(); - let out = Utf8Path::from_path(dir.path()).unwrap(); - DotnetGenerator - .generate(&api, out, &DotnetConfig::default()) - .unwrap(); - let dotnet_dir = out.join("dotnet"); - let cs_files: Vec<_> = std::fs::read_dir(&dotnet_dir) - .unwrap() - .filter_map(|e| e.ok()) - .filter(|e| e.path().extension().map(|x| x == "cs").unwrap_or(false)) - .collect(); - assert!(!cs_files.is_empty(), "expected .cs files"); - let cs = std::fs::read_to_string(cs_files[0].path()).unwrap(); - // A record is a plain sealed data class with typed get-only - // properties and a positional constructor; builders are gone. - assert!( - cs.contains("public sealed class Contact"), - "missing record class: {cs}" - ); - assert!( - cs.contains("public string Name { get; }") && cs.contains("public int Age { get; }"), - "missing typed properties: {cs}" - ); - assert!( - cs.contains("public Contact(string name, int age)"), - "missing positional constructor: {cs}" - ); - // The value-buffer pack/unpack pair replaces every C symbol. - assert!( - cs.contains("internal void WriteTo(WeaveFFIBufferWriter writer)") - && cs.contains("internal static Contact ReadFrom(WeaveFFIBufferReader reader)"), - "missing pack/unpack pair: {cs}" - ); - assert!( - cs.contains("writer.WriteString(Name);") && cs.contains("writer.WriteI32(Age);"), - "missing field encoding: {cs}" - ); - assert!( - !cs.contains("ContactBuilder") && !cs.contains("weaveffi_contacts_Contact_create"), - "builder machinery must be gone: {cs}" - ); - assert!( - !cs.contains("class Contact : IDisposable"), - "records must not be disposable: {cs}" - ); -} - -#[test] -fn dotnet_generates_csproj() { - let api = make_api(vec![simple_module(vec![])]); - - let tmp = std::env::temp_dir().join("weaveffi_test_dotnet_csproj"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - DotnetGenerator - .generate(&api, out_dir, &DotnetConfig::default()) - .unwrap(); - - let csproj_path = tmp.join("dotnet/WeaveFFI.csproj"); - assert!(csproj_path.exists(), ".csproj file must exist"); - let csproj = std::fs::read_to_string(&csproj_path).unwrap(); - assert!( - csproj.contains(r#"Sdk="Microsoft.NET.Sdk""#), - "missing SDK attribute: {csproj}" - ); - assert!( - csproj.contains("net8.0"), - "missing target framework: {csproj}" - ); - assert!( - csproj.contains("WeaveFFI"), - "missing package id: {csproj}" - ); - assert!( - csproj.contains("0.1.0"), - "missing version: {csproj}" - ); - - let nuspec_path = tmp.join("dotnet/WeaveFFI.nuspec"); - assert!(nuspec_path.exists(), ".nuspec file must exist"); - let nuspec = std::fs::read_to_string(&nuspec_path).unwrap(); - assert!( - nuspec.contains("WeaveFFI"), - "missing nuspec id: {nuspec}" - ); - - let readme_path = tmp.join("dotnet/README.md"); - assert!(readme_path.exists(), "README.md must exist"); - let readme = std::fs::read_to_string(&readme_path).unwrap(); - assert!( - readme.contains("dotnet build"), - "missing build instructions: {readme}" - ); - assert!( - readme.contains("dotnet pack"), - "missing pack instructions: {readme}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn cs_type_mapping() { - assert_eq!(cs_type(&TypeRef::I32), "int"); - assert_eq!(cs_type(&TypeRef::U32), "uint"); - assert_eq!(cs_type(&TypeRef::I64), "long"); - assert_eq!(cs_type(&TypeRef::F64), "double"); - assert_eq!(cs_type(&TypeRef::Bool), "bool"); - assert_eq!(cs_type(&TypeRef::StringUtf8), "string"); - assert_eq!(cs_type(&TypeRef::Handle), "ulong"); - assert_eq!(cs_type(&TypeRef::Bytes), "byte[]"); - assert_eq!(cs_type(&TypeRef::Record("Foo".into())), "Foo"); - assert_eq!(cs_type(&TypeRef::Enum("Bar".into())), "Bar"); - assert_eq!(cs_type(&TypeRef::Optional(Box::new(TypeRef::I32))), "int?"); - assert_eq!( - cs_type(&TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - "string?" - ); - assert_eq!( - cs_type(&TypeRef::Optional(Box::new(TypeRef::Record("X".into())))), - "X?" - ); - assert_eq!(cs_type(&TypeRef::List(Box::new(TypeRef::I32))), "int[]"); - assert_eq!( - cs_type(&TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32) - )), - "Dictionary" - ); -} - -#[test] -fn pinvoke_type_mapping() { - assert_eq!(pinvoke_type(&TypeRef::I32), "int"); - // C `bool` is one byte, not int-widened. - assert_eq!(pinvoke_type(&TypeRef::Bool), "byte"); - assert_eq!(pinvoke_type(&TypeRef::RichEnum("Foo".into())), "IntPtr"); - assert_eq!(pinvoke_type(&TypeRef::StringUtf8), "IntPtr"); - assert_eq!(pinvoke_type(&TypeRef::Handle), "ulong"); - assert_eq!(pinvoke_type(&TypeRef::Bytes), "IntPtr"); - assert_eq!(pinvoke_type(&TypeRef::Record("Foo".into())), "IntPtr"); - assert_eq!(pinvoke_type(&TypeRef::Enum("Bar".into())), "int"); - assert_eq!( - pinvoke_type(&TypeRef::Optional(Box::new(TypeRef::I32))), - "IntPtr" - ); -} - -#[test] -fn simple_i32_function() { - let api = make_api(vec![simple_module(vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }])]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!(cs.contains("namespace WeaveFFI"), "missing namespace: {cs}"); - assert!(cs.contains("DllImport"), "missing DllImport: {cs}"); - assert!(cs.contains("weaveffi_math_add"), "missing C symbol: {cs}"); - assert!( - cs.contains("CallingConvention.Cdecl"), - "missing Cdecl: {cs}" - ); - assert!( - cs.contains("public static int Add("), - "missing wrapper method: {cs}" - ); - assert!( - cs.contains("WeaveFFIError.Check(err)"), - "missing error check: {cs}" - ); -} - -#[test] -fn void_function() { - let api = make_api(vec![simple_module(vec![Function { - name: "reset".into(), - params: vec![], - returns: None, - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }])]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("public static void Reset()"), - "missing void wrapper: {cs}" - ); - assert!( - cs.contains("static extern void weaveffi_math_reset"), - "missing void P/Invoke: {cs}" - ); -} - -#[test] -fn bool_uses_int_marshaling() { - let api = make_api(vec![simple_module(vec![Function { - name: "is_valid".into(), - params: vec![Param { - name: "flag".into(), - ty: TypeRef::Bool, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Bool), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }])]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("flag ? 1 : 0"), - "missing bool-to-int conversion: {cs}" - ); - assert!( - cs.contains("result != 0"), - "missing int-to-bool conversion: {cs}" - ); -} - -#[test] -fn enum_generation() { - let api = make_api(vec![Module { - name: "paint".into(), - functions: vec![Function { - name: "mix".into(), - params: vec![Param { - name: "a".into(), - ty: TypeRef::Enum("Color".into()), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Enum("Color".into())), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![EnumDef { - name: "Color".into(), - doc: Some("Primary colors".into()), - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Blue".into(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!(cs.contains("public enum Color"), "missing enum: {cs}"); - assert!(cs.contains("Red = 0"), "missing Red: {cs}"); - assert!(cs.contains("Green = 1"), "missing Green: {cs}"); - assert!(cs.contains("Blue = 2"), "missing Blue: {cs}"); - assert!( - cs.contains("Primary colors"), - "missing doc: {cs}" - ); - assert!(cs.contains("(int)a"), "missing enum-to-int cast: {cs}"); - assert!( - cs.contains("(Color)result"), - "missing int-to-enum cast: {cs}" - ); -} - -#[test] -fn struct_is_sealed_value_class_with_doc() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: Some("A contact record".into()), - fields: vec![ - StructField { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "age".into(), - ty: TypeRef::I32, - doc: None, - }, - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("public sealed class Contact"), - "missing sealed value class: {cs}" - ); - assert!( - cs.contains("public Contact(string firstName, int age)"), - "missing positional constructor: {cs}" - ); - assert!( - cs.contains("A contact record"), - "missing doc: {cs}" - ); - // Records hold no native resources: no handle, no IDisposable. - assert!( - !cs.contains("Contact : IDisposable") && !cs.contains("internal Contact(IntPtr"), - "record must not wrap a handle: {cs}" - ); -} - -#[test] -fn struct_has_property_getters() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - StructField { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "age".into(), - ty: TypeRef::I32, - doc: None, - }, - StructField { - name: "active".into(), - ty: TypeRef::Bool, - doc: None, - }, - StructField { - name: "role".into(), - ty: TypeRef::Enum("Role".into()), - doc: None, - }, - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("public string FirstName { get; }"), - "missing FirstName property: {cs}" - ); - assert!( - cs.contains("public int Age { get; }"), - "missing Age property: {cs}" - ); - assert!( - cs.contains("public bool Active { get; }"), - "missing Active property: {cs}" - ); - assert!( - cs.contains("public Role Role { get; }"), - "missing Role property: {cs}" - ); - // WriteTo serializes each field per the wire format; ReadFrom is the - // exact inverse. No getter symbols cross the ABI anymore. - assert!( - cs.contains("writer.WriteString(FirstName);") - && cs.contains("writer.WriteI32(Age);") - && cs.contains("writer.WriteBool(Active);") - && cs.contains("writer.WriteI32((int)Role);"), - "missing field encodings: {cs}" - ); - assert!( - cs.contains("var fFirstName = reader.ReadString();") - && cs.contains("var fAge = reader.ReadI32();") - && cs.contains("var fActive = reader.ReadBool();") - && cs.contains("var fRole = (Role)reader.ReadI32();"), - "missing field decodings: {cs}" - ); - assert!( - !cs.contains("weaveffi_contacts_Contact_get_first_name"), - "getter symbols must be gone: {cs}" - ); -} - -#[test] -fn struct_has_no_dispose_or_finalizer() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "id".into(), - ty: TypeRef::I32, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - // A record owns no native memory, so nothing to dispose or finalize. - assert!( - cs.contains("public sealed class Contact"), - "missing record class: {cs}" - ); - assert!( - !cs.contains("weaveffi_contacts_Contact_destroy"), - "destroy symbol must be gone: {cs}" - ); - assert!(!cs.contains("~Contact()"), "finalizer must be gone: {cs}"); -} - -#[test] -fn struct_emits_no_pinvoke_declarations() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - StructField { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "age".into(), - ty: TypeRef::I32, - doc: None, - }, - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - // Records cross the ABI only inside value buffers, so no per-record - // P/Invoke declarations exist. The shared runtime imports remain. - assert!( - !cs.contains("weaveffi_contacts_Contact_"), - "record symbols must be gone: {cs}" - ); - assert!( - cs.contains("internal static extern void weaveffi_free_bytes(IntPtr ptr, UIntPtr len);"), - "missing free_bytes runtime import: {cs}" - ); - assert!( - cs.contains("internal static extern void weaveffi_error_clear(ref WeaveFFIError err);"), - "missing error_clear runtime import: {cs}" - ); -} - -#[test] -fn string_function_uses_utf8() { - let api = make_api(vec![Module { - name: "text".into(), - functions: vec![Function { - name: "echo".into(), - params: vec![Param { - name: "msg".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("Marshal.PtrToStringUTF8(result)"), - "missing PtrToStringUTF8: {cs}" - ); - assert!( - cs.contains("Marshal.StringToCoTaskMemUTF8(msg)"), - "missing StringToCoTaskMemUTF8: {cs}" - ); - assert!( - cs.contains("Marshal.FreeCoTaskMem(msgPtr)"), - "missing FreeCoTaskMem: {cs}" - ); - assert!( - cs.contains("weaveffi_free_string(result)"), - "missing free_string: {cs}" - ); -} - -#[test] -fn safe_cs_name_escapes_keywords() { - assert_eq!(safe_cs_name("string"), "@string"); - assert_eq!(safe_cs_name("class"), "@class"); - assert_eq!(safe_cs_name("return"), "@return"); - assert_eq!(safe_cs_name("name"), "name"); - assert_eq!(safe_cs_name("id"), "id"); -} - -#[test] -fn native_methods_class() { - let api = make_api(vec![simple_module(vec![Function { - name: "add".into(), - params: vec![], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }])]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("internal static class NativeMethods"), - "missing NativeMethods: {cs}" - ); - assert!( - cs.contains("weaveffi_free_string"), - "missing free_string P/Invoke: {cs}" - ); - assert!( - cs.contains("weaveffi_free_bytes"), - "missing free_bytes P/Invoke: {cs}" - ); -} - -#[test] -fn pinvoke_has_error_param() { - let api = make_api(vec![simple_module(vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }])]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("ref WeaveFFIError err"), - "missing error param in P/Invoke: {cs}" - ); -} - -#[test] -fn header_has_using_statements() { - let api = make_api(vec![]); - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!(cs.contains("using System;"), "missing System: {cs}"); - assert!( - cs.contains("using System.Runtime.InteropServices;"), - "missing InteropServices: {cs}" - ); - assert!( - cs.contains("using System.Collections.Generic;"), - "missing Collections.Generic: {cs}" - ); -} - -#[test] -fn optional_types() { - assert_eq!(cs_type(&TypeRef::Optional(Box::new(TypeRef::I32))), "int?"); - assert_eq!( - cs_type(&TypeRef::Optional(Box::new(TypeRef::Bool))), - "bool?" - ); - assert_eq!( - cs_type(&TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - "string?" - ); - assert_eq!( - cs_type(&TypeRef::Optional(Box::new(TypeRef::Enum("Foo".into())))), - "Foo?" - ); - assert_eq!( - cs_type(&TypeRef::Optional(Box::new(TypeRef::Record("Bar".into())))), - "Bar?" - ); -} - -#[test] -fn struct_return_decodes_value_buffer() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "get_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Handle, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("public static Contact GetContact(ulong id)"), - "missing method sig: {cs}" - ); - // A record return arrives as a producer-owned value buffer: the - // wrapper copies it, frees it, then decodes the copy. - assert!(cs.contains("out var outLen"), "missing outLen slot: {cs}"); - assert!( - cs.contains("NativeMethods.weaveffi_free_bytes(result, outLen);"), - "missing buffer release: {cs}" - ); - assert!( - cs.contains("var value = Contact.ReadFrom(valueReader);") - && cs.contains("valueReader.ExpectEnd();") - && cs.contains("return value;"), - "missing buffer decode: {cs}" - ); -} - -#[test] -fn list_return_type() { - let api = make_api(vec![Module { - name: "store".into(), - functions: vec![Function { - name: "get_ids".into(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::I32))), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("public static int[] GetIds()"), - "missing list return method: {cs}" - ); - assert!(cs.contains("out var outLen"), "missing outLen: {cs}"); - // The list crosses as one value buffer, not parallel arrays: the - // wrapper copies it, frees it, then decodes count-prefixed elements. - assert!( - cs.contains("NativeMethods.weaveffi_free_bytes(result, outLen);"), - "missing value-buffer release: {cs}" - ); - assert!( - cs.contains("var valueCount = valueReader.ReadLen();") - && cs.contains("var value = new int[valueCount];") - && cs.contains("var valueItem = valueReader.ReadI32();"), - "missing list decode loop: {cs}" - ); -} - -#[test] -fn map_return_type() { - let api = make_api(vec![Module { - name: "store".into(), - functions: vec![Function { - name: "get_scores".into(), - params: vec![], - returns: Some(TypeRef::Map(Box::new(TypeRef::I32), Box::new(TypeRef::F64))), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("public static Dictionary GetScores()"), - "missing map return: {cs}" - ); - // Parallel key/value buffers are gone: the map crosses as one value - // buffer decoded as count-prefixed alternating pairs. - assert!( - !cs.contains("out var outKeys") && !cs.contains("out var outValues"), - "parallel buffers must be gone: {cs}" - ); - assert!( - cs.contains("var value = new Dictionary(valueCount);") - && cs.contains("var valueKey = valueReader.ReadI32();") - && cs.contains("var valueVal = valueReader.ReadF64();") - && cs.contains("value[valueKey] = valueVal;"), - "missing map decode loop: {cs}" - ); - assert!( - cs.contains("NativeMethods.weaveffi_free_bytes(result, outLen);"), - "missing value-buffer release: {cs}" - ); -} - -#[test] -fn struct_optional_string_field() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("public string? Email { get; }"), - "missing optional string property: {cs}" - ); - // The optional encodes as a flag byte plus the value when present, - // and decodes back into a nullable local. - assert!( - cs.contains("if (Email != null)") - && cs.contains("writer.WriteOptionFlag(true);") - && cs.contains("writer.WriteString(Email!);") - && cs.contains("writer.WriteOptionFlag(false);"), - "missing optional encode: {cs}" - ); - assert!( - cs.contains("string? fEmail = null;") - && cs.contains("if (reader.ReadOptionFlag())") - && cs.contains("var fEmailValue = reader.ReadString();"), - "missing optional decode: {cs}" - ); -} - -#[test] -fn optional_string_param_marshalling() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "create".into(), - params: vec![ - Param { - name: "name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }, - Param { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::Handle), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - // Plain strings still cross as C strings. - assert!( - cs.contains("StringToCoTaskMemUTF8(name)"), - "missing name marshal: {cs}" - ); - assert!( - cs.contains("FreeCoTaskMem(namePtr)"), - "missing name cleanup: {cs}" - ); - // The optional string is buffered: flag byte plus value, pinned and - // passed as (ptr, len), then unpinned. - assert!( - cs.contains("var emailWriter = new WeaveFFIBufferWriter();") - && cs.contains("if (email != null)") - && cs.contains("emailWriter.WriteOptionFlag(true);") - && cs.contains("emailWriter.WriteString(email!);"), - "missing optional buffer encode: {cs}" - ); - assert!( - cs.contains("emailPin.AddrOfPinnedObject(), (UIntPtr)emailBuf.Length"), - "missing (ptr, len) call args: {cs}" - ); - assert!(cs.contains("emailPin.Free();"), "missing unpin: {cs}"); -} - -#[test] -fn comprehensive_contacts_api() { - let api = make_api(vec![Module { - name: "contacts".into(), - enums: vec![EnumDef { - name: "ContactType".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Personal".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Work".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: None, - }, - StructField { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - doc: None, - }, - StructField { - name: "contact_type".into(), - ty: TypeRef::Enum("ContactType".into()), - doc: None, - }, - ], - }], - functions: vec![ - Function { - name: "create_contact".into(), - params: vec![ - Param { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }, - Param { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - mutable: false, - doc: None, - }, - Param { - name: "contact_type".into(), - ty: TypeRef::Enum("ContactType".into()), - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::Handle), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }, - Function { - name: "get_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Handle, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }, - Function { - name: "count_contacts".into(), - params: vec![], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }, - ], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let tmp = std::env::temp_dir().join("weaveffi_test_dotnet_contacts_v2"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - DotnetGenerator - .generate( - &api, - out_dir, - &DotnetConfig { - strip_module_prefix: true, - ..DotnetConfig::default() - }, - ) - .unwrap(); - - let cs = std::fs::read_to_string(tmp.join("dotnet/WeaveFFI.cs")).unwrap(); - - assert!(cs.contains("public enum ContactType")); - assert!(cs.contains("Personal = 0")); - assert!(cs.contains("Work = 1")); - - // The record is a plain value class with typed properties and the - // value-buffer pack/unpack pair; no handle or C symbols remain. - assert!(cs.contains("public sealed class Contact")); - assert!(cs.contains( - "public Contact(long id, string firstName, string? email, ContactType contactType)" - )); - assert!(cs.contains("public long Id { get; }")); - assert!(cs.contains("public string FirstName { get; }")); - assert!(cs.contains("public string? Email { get; }")); - assert!(cs.contains("public ContactType ContactType { get; }")); - assert!(cs.contains("internal void WriteTo(WeaveFFIBufferWriter writer)")); - assert!(cs.contains("internal static Contact ReadFrom(WeaveFFIBufferReader reader)")); - assert!(!cs.contains("weaveffi_contacts_Contact_")); - - assert!(cs.contains("weaveffi_contacts_create_contact(")); - assert!(cs.contains("weaveffi_contacts_get_contact(")); - assert!(cs.contains("weaveffi_contacts_count_contacts(")); - - assert!(cs.contains("public static class Contacts")); - assert!(cs.contains("public static ulong CreateContact(")); - assert!(cs.contains("public static Contact GetContact(")); - assert!(cs.contains("public static int CountContacts(")); - - assert!(cs.contains("internal static class NativeMethods")); - assert!(cs.contains("CallingConvention.Cdecl")); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn generate_dotnet_basic() { - let api = make_api(vec![simple_module(vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }])]); - - let tmp = std::env::temp_dir().join("weaveffi_test_dotnet_basic"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - DotnetGenerator - .generate( - &api, - out_dir, - &DotnetConfig { - strip_module_prefix: true, - ..DotnetConfig::default() - }, - ) - .unwrap(); - let cs = std::fs::read_to_string(tmp.join("dotnet/WeaveFFI.cs")).unwrap(); - - assert!( - cs.contains("EntryPoint = \"weaveffi_math_add\""), - "missing P/Invoke EntryPoint: {cs}" - ); - assert!( - cs.contains( - "internal static extern int weaveffi_math_add(int a, int b, ref WeaveFFIError err)" - ), - "missing P/Invoke declaration: {cs}" - ); - assert!( - cs.contains("public static int Add(int a, int b)"), - "missing wrapper method signature: {cs}" - ); - assert!( - cs.contains("NativeMethods.weaveffi_math_add(a, b, ref err)"), - "missing P/Invoke call in wrapper: {cs}" - ); - assert!( - cs.contains("WeaveFFIError.Check(err)"), - "missing error check in wrapper: {cs}" - ); - assert!( - cs.contains("return result;"), - "missing return statement: {cs}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn generate_dotnet_with_structs() { - let api = make_api(vec![Module { - name: "crm".into(), - functions: vec![], - structs: vec![StructDef { - name: "Person".into(), - doc: Some("A person record".into()), - fields: vec![ - StructField { - name: "full_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "age".into(), - ty: TypeRef::I32, - doc: None, - }, - StructField { - name: "score".into(), - ty: TypeRef::F64, - doc: None, - }, - StructField { - name: "active".into(), - ty: TypeRef::Bool, - doc: None, - }, - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - - assert!( - cs.contains("public sealed class Person"), - "missing sealed value class: {cs}" - ); - assert!( - cs.contains("A person record"), - "missing doc summary: {cs}" - ); - assert!( - cs.contains("public Person(string fullName, int age, double score, bool active)"), - "missing positional constructor: {cs}" - ); - - assert!( - cs.contains("public string FullName { get; }"), - "missing FullName property: {cs}" - ); - assert!( - cs.contains("public int Age { get; }"), - "missing Age property: {cs}" - ); - assert!( - cs.contains("public double Score { get; }"), - "missing Score property: {cs}" - ); - assert!( - cs.contains("public bool Active { get; }"), - "missing Active property: {cs}" - ); - - // The pack/unpack pair covers every field in declaration order. - assert!( - cs.contains("writer.WriteString(FullName);") - && cs.contains("writer.WriteI32(Age);") - && cs.contains("writer.WriteF64(Score);") - && cs.contains("writer.WriteBool(Active);"), - "missing field encodings: {cs}" - ); - assert!( - cs.contains("var fFullName = reader.ReadString();") - && cs.contains("var fAge = reader.ReadI32();") - && cs.contains("var fScore = reader.ReadF64();") - && cs.contains("var fActive = reader.ReadBool();") - && cs.contains("return new Person(fFullName, fAge, fScore, fActive);"), - "missing field decodings: {cs}" - ); - - // No native lifecycle remains for records. - assert!( - !cs.contains("weaveffi_crm_Person_") && !cs.contains("~Person()"), - "record C symbols must be gone: {cs}" - ); -} - -#[test] -fn generate_dotnet_with_enums() { - let api = make_api(vec![Module { - name: "status".into(), - functions: vec![Function { - name: "get_status".into(), - params: vec![], - returns: Some(TypeRef::Enum("Priority".into())), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![EnumDef { - name: "Priority".into(), - doc: Some("Task priority levels".into()), - variants: vec![ - EnumVariant { - name: "Low".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Medium".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "High".into(), - value: 2, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Critical".into(), - value: 3, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - - assert!( - cs.contains("Task priority levels"), - "missing enum doc: {cs}" - ); - assert!( - cs.contains("public enum Priority"), - "missing enum declaration: {cs}" - ); - assert!(cs.contains("Low = 0,"), "missing Low variant: {cs}"); - assert!(cs.contains("Medium = 1,"), "missing Medium variant: {cs}"); - assert!(cs.contains("High = 2,"), "missing High variant: {cs}"); - assert!( - cs.contains("Critical = 3,"), - "missing Critical variant: {cs}" - ); - - assert!( - cs.contains("(Priority)result"), - "missing enum return cast: {cs}" - ); - assert!( - cs.contains("public static Priority GetStatus()"), - "missing wrapper returning enum: {cs}" - ); -} - -#[test] -fn generate_dotnet_with_optionals() { - let api = make_api(vec![Module { - name: "config".into(), - functions: vec![Function { - name: "update".into(), - params: vec![ - Param { - name: "label".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - mutable: false, - doc: None, - }, - Param { - name: "count".into(), - ty: TypeRef::Optional(Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::Optional(Box::new(TypeRef::I64))), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Settings".into(), - doc: None, - fields: vec![ - StructField { - name: "nickname".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - doc: None, - }, - StructField { - name: "max_retries".into(), - ty: TypeRef::Optional(Box::new(TypeRef::I32)), - doc: None, - }, - StructField { - name: "threshold".into(), - ty: TypeRef::Optional(Box::new(TypeRef::F64)), - doc: None, - }, - StructField { - name: "enabled".into(), - ty: TypeRef::Optional(Box::new(TypeRef::Bool)), - doc: None, - }, - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - - assert!( - cs.contains("public static long? Update(string? label, int? count)"), - "missing Nullable wrapper sig: {cs}" - ); - - // Optional parameters are buffered: a flag byte plus the value when - // present, pinned and passed as (ptr, len). - assert!( - cs.contains("var labelWriter = new WeaveFFIBufferWriter();") - && cs.contains("labelWriter.WriteString(label!);"), - "missing optional string param encode: {cs}" - ); - assert!( - cs.contains("var countWriter = new WeaveFFIBufferWriter();") - && cs.contains("countWriter.WriteI32(count.Value);"), - "missing optional int param encode: {cs}" - ); - - // The optional return decodes from the freed-after-copy value buffer. - assert!( - cs.contains("long? value = null;") - && cs.contains("if (valueReader.ReadOptionFlag())") - && cs.contains("var valueValue = valueReader.ReadI64();"), - "missing optional return decode: {cs}" - ); - assert!( - cs.contains("NativeMethods.weaveffi_free_bytes(result, outLen);"), - "missing return buffer release: {cs}" - ); - - // Optional record fields become nullable properties with flag-byte - // encodings; no boxed-scalar pointers remain. - assert!( - cs.contains("public string? Nickname { get; }"), - "missing optional string property: {cs}" - ); - assert!( - cs.contains("public int? MaxRetries { get; }"), - "missing optional int property: {cs}" - ); - assert!( - cs.contains("public double? Threshold { get; }"), - "missing optional f64 property: {cs}" - ); - assert!( - cs.contains("public bool? Enabled { get; }"), - "missing optional bool property: {cs}" - ); - assert!( - cs.contains("writer.WriteF64(Threshold.Value);") - && cs.contains("writer.WriteBool(Enabled.Value);"), - "missing optional field encodings: {cs}" - ); - assert!( - cs.contains("bool? fEnabled = null;") && cs.contains("double? fThreshold = null;"), - "missing optional field decodings: {cs}" - ); - assert!( - !cs.contains("Marshal.ReadByte(ptr)"), - "boxed-scalar pointers must be gone: {cs}" - ); -} - -#[test] -fn generate_dotnet_with_lists() { - let api = make_api(vec![Module { - name: "data".into(), - functions: vec![ - Function { - name: "get_ids".into(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::I32))), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }, - Function { - name: "get_values".into(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::F64))), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }, - Function { - name: "get_timestamps".into(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::I64))), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }, - ], - structs: vec![StructDef { - name: "Record".into(), - doc: None, - fields: vec![StructField { - name: "tags".into(), - ty: TypeRef::List(Box::new(TypeRef::I32)), - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - - assert!( - cs.contains("public static int[] GetIds()"), - "missing int[] return: {cs}" - ); - assert!( - cs.contains("public static double[] GetValues()"), - "missing double[] return: {cs}" - ); - assert!( - cs.contains("public static long[] GetTimestamps()"), - "missing long[] return: {cs}" - ); - assert!( - cs.contains("out var outLen"), - "missing outLen parameter: {cs}" - ); - // Each list decodes from its own value buffer: count prefix, typed - // elements, then the producer buffer is released. - assert!( - cs.contains("var value = new int[valueCount];") - && cs.contains("var value = new double[valueCount];") - && cs.contains("var value = new long[valueCount];"), - "missing typed element arrays: {cs}" - ); - assert!( - cs.contains("var valueItem = valueReader.ReadI32();") - && cs.contains("var valueItem = valueReader.ReadF64();") - && cs.contains("var valueItem = valueReader.ReadI64();"), - "missing element decodes: {cs}" - ); - assert!( - cs.contains("NativeMethods.weaveffi_free_bytes(result, outLen);"), - "missing value-buffer release: {cs}" - ); - - // A list-typed record field is a plain typed property with a - // count-prefixed encoding. - assert!( - cs.contains("public int[] Tags { get; }"), - "missing list property: {cs}" - ); - assert!( - cs.contains("writer.WriteLen(Tags.Length);"), - "missing list field encode: {cs}" - ); -} - -#[test] -fn generate_dotnet_full_contacts() { - let api = make_api(vec![Module { - name: "contacts".into(), - enums: vec![EnumDef { - name: "ContactType".into(), - doc: Some("Type of contact".into()), - variants: vec![ - EnumVariant { - name: "Personal".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Business".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Government".into(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: Some("A contact entry".into()), - fields: vec![ - StructField { - name: "id".into(), - ty: TypeRef::Handle, - doc: None, - }, - StructField { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "last_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - doc: None, - }, - StructField { - name: "age".into(), - ty: TypeRef::I32, - doc: None, - }, - StructField { - name: "active".into(), - ty: TypeRef::Bool, - doc: None, - }, - StructField { - name: "contact_type".into(), - ty: TypeRef::Enum("ContactType".into()), - doc: None, - }, - StructField { - name: "scores".into(), - ty: TypeRef::List(Box::new(TypeRef::I32)), - doc: None, - }, - ], - }], - functions: vec![ - Function { - name: "create_contact".into(), - params: vec![ - Param { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }, - Param { - name: "last_name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }, - Param { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - mutable: false, - doc: None, - }, - Param { - name: "contact_type".into(), - ty: TypeRef::Enum("ContactType".into()), - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::Handle), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }, - Function { - name: "get_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Handle, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }, - Function { - name: "list_contacts".into(), - params: vec![Param { - name: "contact_type".into(), - ty: TypeRef::Optional(Box::new(TypeRef::Enum("ContactType".into()))), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::List(Box::new(TypeRef::Record("Contact".into())))), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }, - Function { - name: "delete_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Handle, - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }, - Function { - name: "count_contacts".into(), - params: vec![], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }, - ], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let tmp = std::env::temp_dir().join("weaveffi_test_dotnet_full_contacts"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - DotnetGenerator - .generate( - &api, - out_dir, - &DotnetConfig { - strip_module_prefix: true, - ..DotnetConfig::default() - }, - ) - .unwrap(); - let cs = std::fs::read_to_string(tmp.join("dotnet/WeaveFFI.cs")).unwrap(); - - // Enum - assert!(cs.contains("public enum ContactType"), "missing enum: {cs}"); - assert!(cs.contains("Personal = 0,"), "missing Personal: {cs}"); - assert!(cs.contains("Business = 1,"), "missing Business: {cs}"); - assert!(cs.contains("Government = 2,"), "missing Government: {cs}"); - assert!( - cs.contains("Type of contact"), - "missing enum doc: {cs}" - ); - - // Struct as a plain value class - assert!( - cs.contains("public sealed class Contact"), - "missing sealed value class: {cs}" - ); - assert!( - cs.contains("A contact entry"), - "missing struct doc: {cs}" - ); - assert!( - !cs.contains("~Contact()") && !cs.contains("weaveffi_contacts_Contact_"), - "record lifecycle symbols must be gone: {cs}" - ); - - // Typed properties - assert!( - cs.contains("public ulong Id { get; }"), - "missing Id property: {cs}" - ); - assert!( - cs.contains("public string FirstName { get; }"), - "missing FirstName: {cs}" - ); - assert!( - cs.contains("public string LastName { get; }"), - "missing LastName: {cs}" - ); - assert!( - cs.contains("public string? Email { get; }"), - "missing optional Email: {cs}" - ); - assert!(cs.contains("public int Age { get; }"), "missing Age: {cs}"); - assert!( - cs.contains("public bool Active { get; }"), - "missing Active: {cs}" - ); - assert!( - cs.contains("public ContactType ContactType { get; }"), - "missing ContactType property: {cs}" - ); - assert!( - cs.contains("public int[] Scores { get; }"), - "missing Scores list property: {cs}" - ); - - // Pack/unpack pair - assert!( - cs.contains("internal void WriteTo(WeaveFFIBufferWriter writer)") - && cs.contains("internal static Contact ReadFrom(WeaveFFIBufferReader reader)"), - "missing pack/unpack pair: {cs}" - ); - - // P/Invoke declarations - assert!( - cs.contains("weaveffi_contacts_create_contact("), - "missing create_contact P/Invoke: {cs}" - ); - assert!( - cs.contains("weaveffi_contacts_get_contact("), - "missing get_contact P/Invoke: {cs}" - ); - assert!( - cs.contains("weaveffi_contacts_list_contacts("), - "missing list_contacts P/Invoke: {cs}" - ); - assert!( - cs.contains("weaveffi_contacts_delete_contact("), - "missing delete_contact P/Invoke: {cs}" - ); - assert!( - cs.contains("weaveffi_contacts_count_contacts("), - "missing count_contacts P/Invoke: {cs}" - ); - - // Wrapper class - assert!( - cs.contains("public static class Contacts"), - "missing Contacts wrapper class: {cs}" - ); - assert!( - cs.contains("public static ulong CreateContact("), - "missing CreateContact wrapper: {cs}" - ); - assert!( - cs.contains("public static Contact GetContact(ulong id)"), - "missing GetContact wrapper: {cs}" - ); - assert!( - cs.contains("public static Contact[] ListContacts("), - "missing ListContacts wrapper: {cs}" - ); - assert!( - cs.contains("public static void DeleteContact(ulong id)"), - "missing DeleteContact wrapper: {cs}" - ); - assert!( - cs.contains("public static int CountContacts()"), - "missing CountContacts wrapper: {cs}" - ); - - // Supporting output files - assert!( - tmp.join("dotnet/WeaveFFI.csproj").exists(), - ".csproj must exist" - ); - assert!( - tmp.join("dotnet/WeaveFFI.nuspec").exists(), - ".nuspec must exist" - ); - assert!(tmp.join("dotnet/README.md").exists(), "README must exist"); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn dotnet_has_memory_helpers() { - let api = make_api(vec![simple_module(vec![])]); - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("internal static class WeaveFFIHelpers"), - "missing WeaveFFIHelpers class: {cs}" - ); - assert!( - cs.contains("internal static IntPtr StringToPtr(string? s)"), - "missing StringToPtr: {cs}" - ); - assert!( - cs.contains("internal static string? PtrToString(IntPtr ptr)"), - "missing PtrToString: {cs}" - ); - assert!( - cs.contains("internal static void FreePtr(IntPtr ptr)"), - "missing FreePtr: {cs}" - ); - assert!( - cs.contains("Marshal.StringToCoTaskMemUTF8(s)"), - "missing StringToCoTaskMemUTF8 in helper: {cs}" - ); - assert!( - cs.contains("Marshal.FreeCoTaskMem(ptr)"), - "missing FreeCoTaskMem in helper: {cs}" - ); -} - -#[test] -fn dotnet_custom_namespace() { - let api = make_api(vec![simple_module(vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }])]); - - let config = DotnetConfig { - namespace: Some("MyCompany.Bindings".into()), - ..DotnetConfig::default() - }; - - let tmp = std::env::temp_dir().join("weaveffi_test_dotnet_custom_ns"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - DotnetGenerator.generate(&api, out_dir, &config).unwrap(); - - let cs_path = tmp.join("dotnet/MyCompany.Bindings.cs"); - assert!( - cs_path.exists(), - ".cs file should use custom namespace name" - ); - let cs = std::fs::read_to_string(&cs_path).unwrap(); - assert!( - cs.contains("namespace MyCompany.Bindings"), - "namespace should use custom name: {cs}" - ); - - let csproj_path = tmp.join("dotnet/MyCompany.Bindings.csproj"); - assert!(csproj_path.exists(), ".csproj should use custom namespace"); - let csproj = std::fs::read_to_string(&csproj_path).unwrap(); - assert!( - csproj.contains("MyCompany.Bindings"), - "PackageId should use custom namespace: {csproj}" - ); - - let nuspec_path = tmp.join("dotnet/MyCompany.Bindings.nuspec"); - assert!(nuspec_path.exists(), ".nuspec should use custom namespace"); - let nuspec = std::fs::read_to_string(&nuspec_path).unwrap(); - assert!( - nuspec.contains("MyCompany.Bindings"), - "nuspec id should use custom namespace: {nuspec}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn dotnet_strip_module_prefix() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "create_contact".into(), - params: vec![Param { - name: "name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - // Stripping is the default: the per-module static class already - // namespaces the method. - let config = DotnetConfig::default(); - - let tmp = std::env::temp_dir().join("weaveffi_test_dotnet_strip_prefix"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - DotnetGenerator.generate(&api, out_dir, &config).unwrap(); - - let cs = std::fs::read_to_string(tmp.join("dotnet/WeaveFFI.cs")).unwrap(); - assert!( - cs.contains("CreateContact("), - "stripped name should be CreateContact: {cs}" - ); - assert!( - !cs.contains("ContactsCreateContact("), - "should not contain module-prefixed name: {cs}" - ); - assert!( - cs.contains("weaveffi_contacts_create_contact"), - "C ABI call should still use full name: {cs}" - ); - - let no_strip = DotnetConfig { - strip_module_prefix: false, - ..DotnetConfig::default() - }; - let tmp2 = std::env::temp_dir().join("weaveffi_test_dotnet_no_strip_prefix"); - let _ = std::fs::remove_dir_all(&tmp2); - std::fs::create_dir_all(&tmp2).unwrap(); - let out_dir2 = Utf8Path::from_path(&tmp2).expect("valid UTF-8"); - - DotnetGenerator.generate(&api, out_dir2, &no_strip).unwrap(); - - let cs2 = std::fs::read_to_string(tmp2.join("dotnet/WeaveFFI.cs")).unwrap(); - assert!( - cs2.contains("ContactsCreateContact("), - "strip_module_prefix: false should restore module-prefixed names: {cs2}" - ); - - let _ = std::fs::remove_dir_all(&tmp); - let _ = std::fs::remove_dir_all(&tmp2); -} - -#[test] -fn dotnet_deeply_nested_optional() { - let api = make_api(vec![Module { - name: "edge".into(), - functions: vec![Function { - name: "process".into(), - params: vec![Param { - name: "data".into(), - ty: TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional( - Box::new(TypeRef::Record("Contact".into())), - ))))), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("Contact?[]?"), - "should contain deeply nested optional type: {cs}" - ); -} - -#[test] -fn dotnet_map_of_lists() { - let api = make_api(vec![Module { - name: "edge".into(), - functions: vec![Function { - name: "process".into(), - params: vec![Param { - name: "scores".into(), - ty: TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::List(Box::new(TypeRef::I32))), - ), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("Dictionary"), - "should contain map of lists type: {cs}" - ); -} - -#[test] -fn dotnet_enum_keyed_map() { - let api = make_api(vec![Module { - name: "edge".into(), - functions: vec![Function { - name: "process".into(), - params: vec![Param { - name: "contacts".into(), - ty: TypeRef::Map( - Box::new(TypeRef::Enum("Color".into())), - Box::new(TypeRef::Record("Contact".into())), - ), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![EnumDef { - name: "Color".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Blue".into(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("Dictionary"), - "should contain enum-keyed map type: {cs}" - ); -} - -#[test] -fn dotnet_typed_handle_type() { - let api = ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules: vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "get_info".into(), - params: vec![Param { - name: "contact".into(), - ty: TypeRef::TypedHandle("Contact".into()), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }], - generators: None, - package: None, - }); - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - // A typed handle renders as a dedicated readonly wrapper struct, not - // a bare ulong and not the record class. - assert!( - cs.contains("ContactHandle contact"), - "TypedHandle should use the wrapper struct: {cs}" - ); - assert!( - cs.contains("public readonly struct ContactHandle"), - "missing handle wrapper struct: {cs}" - ); - assert!( - cs.contains("contact.Raw"), - "wrapper should pass the raw pointer: {cs}" - ); -} - -#[test] -fn dotnet_no_double_free_on_error() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "find_contact".into(), - params: vec![Param { - name: "name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("StringToCoTaskMemUTF8"), - "string param should be marshalled to unmanaged memory: {cs}" - ); - assert!( - cs.contains("finally") && cs.contains("FreeCoTaskMem"), - "marshalled string should be freed in finally (no double-free of managed string): {cs}" - ); - let find = cs.find("FindContact").expect("FindContact wrapper"); - let slice = &cs[find..]; - let check_rel = slice - .find("WeaveFFIError.Check(err)") - .expect("WeaveFFIError.Check in FindContact"); - let free_rel = slice - .find("NativeMethods.weaveffi_free_bytes(result, outLen);") - .expect("value-buffer release in FindContact"); - let decode_rel = slice - .find("Contact.ReadFrom(valueReader)") - .expect("Contact.ReadFrom in FindContact"); - assert!( - check_rel < free_rel && free_rel < decode_rel, - "error must be checked, the buffer freed once, then decoded: {cs}" - ); - // The record is a value type: nothing to dispose, so no double-free - // hazard on the return path. - assert!( - !cs.contains("Contact : IDisposable") && !cs.contains("~Contact()"), - "record must not be disposable: {cs}" - ); -} - -#[test] -fn dotnet_null_check_on_optional_return() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "find_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Optional(Box::new(TypeRef::Record( - "Contact".into(), - )))), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - // The optional record decodes from the value buffer: a flag byte - // gates the nested record read, and absent means null. - assert!( - cs.contains("public static Contact? FindContact(int id)"), - "missing nullable wrapper sig: {cs}" - ); - assert!( - cs.contains("Contact? value = null;") - && cs.contains("if (valueReader.ReadOptionFlag())") - && cs.contains("var valueValue = Contact.ReadFrom(valueReader);"), - "optional record return should decode via flag byte: {cs}" - ); -} - -#[test] -fn dotnet_async_returns_task() { - let api = make_api(vec![Module { - name: "tasks".into(), - functions: vec![Function { - name: "run".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: None, - r#async: true, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("async Task<"), - "missing async Task< in signature: {cs}" - ); -} - -#[test] -fn dotnet_async_uses_tcs() { - let api = make_api(vec![Module { - name: "tasks".into(), - functions: vec![Function { - name: "run".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: None, - r#async: true, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("TaskCompletionSource"), - "missing TaskCompletionSource: {cs}" - ); -} - -/// `GCHandle.Alloc(callback, GCHandleType.Normal)` (the .NET equivalent -/// of pinning the delegate so the GC won't reclaim it while the C side -/// owns a function pointer to it) must be balanced by exactly one -/// `GCHandle.FromIntPtr(context).Free()` in the C callback after the -/// `TaskCompletionSource` is resolved. -#[test] -fn dotnet_async_pins_callback_for_lifetime() { - let api = make_api(vec![Module { - name: "tasks".into(), - functions: vec![Function { - name: "run".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: None, - r#async: true, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("GCHandle.Alloc(callback, GCHandleType.Normal)"), - "missing GCHandle.Alloc(..., Normal): {cs}" - ); - assert!( - cs.contains("GCHandle.ToIntPtr(gcHandle)"), - "GCHandle must be passed as the C context: {cs}" - ); - assert!( - cs.contains("GCHandle.FromIntPtr(context).Free()"), - "missing GCHandle.Free in callback: {cs}" - ); -} - -/// A module with one async function per given return type, named `run0`, -/// `run1`, ... in order, plus a `Contact` record for object results. -fn async_api(returns: Vec>) -> ResolvedApi { - let functions = returns - .into_iter() - .enumerate() - .map(|(i, ret)| Function { - name: format!("run{i}"), - params: vec![], - returns: ret, - doc: None, - r#async: true, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }) - .collect(); - make_api(vec![Module { - name: "tasks".into(), - functions, - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "id".into(), - ty: TypeRef::Handle, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]) -} - -/// Async result buffers are owned by the consumer (`AsyncProtocol` -/// clause 2): strings and bytes are deep-copied inside the callback and -/// then released through the runtime free symbols. -#[test] -fn dotnet_async_owned_results_copied_then_freed() { - let cs = render_csharp( - &async_api(vec![ - Some(TypeRef::StringUtf8), - Some(TypeRef::Bytes), - Some(TypeRef::Optional(Box::new(TypeRef::I64))), - ]), - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - // String result: copied, then freed. - assert!( - cs.contains("var str = Marshal.PtrToStringUTF8(result) ?? \"\";") - && cs.contains("tcs.SetResult(str);"), - "async string result must copy: {cs}" - ); - assert!( - cs.contains("NativeMethods.weaveffi_free_string(result);"), - "async string result is owned and must be freed after copying: {cs}" - ); - // Bytes result: copied via the (result, resultLen) pair, then freed. - assert!( - cs.contains("Marshal.Copy(result, arr, 0, (int)resultLen);"), - "async bytes result must copy: {cs}" - ); - assert!( - cs.contains("NativeMethods.weaveffi_free_bytes(result, resultLen);"), - "async bytes result is owned and must be freed after copying: {cs}" - ); - // Buffered optional result: the owned buffer is copied, freed, and - // decoded inside the callback. - assert!( - cs.contains("Marshal.Copy(result, resultBuf, 0, (int)resultLen);") - && cs.contains("long? value = null;") - && cs.contains("var valueValue = valueReader.ReadI64();") - && cs.contains("tcs.SetResult(value);"), - "async optional result must decode the owned buffer: {cs}" - ); -} - -/// Record, list, and map async results all arrive as one owned -/// `(result, resultLen)` value buffer: the callback copies, frees, and -/// decodes it before completing the task. -#[test] -fn dotnet_async_buffered_results_decoded() { - let cs = render_csharp( - &async_api(vec![ - Some(TypeRef::Record("Contact".into())), - Some(TypeRef::List(Box::new(TypeRef::StringUtf8))), - Some(TypeRef::List(Box::new(TypeRef::Record("Contact".into())))), - Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )), - ]), - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - // Every buffered result arrives through the (result, resultLen) pair. - assert!( - cs.contains("IntPtr result, UIntPtr resultLen"), - "async buffered delegate must carry the length slot: {cs}" - ); - // Record result decoded from the local copy. - assert!( - cs.contains("var value = Contact.ReadFrom(valueReader);") - && cs.contains("tcs.SetResult(value);"), - "async record result must decode: {cs}" - ); - // List elements decode in place: strings copy, records recurse. - assert!( - cs.contains("var valueItem = valueReader.ReadString();"), - "async string list elements must decode: {cs}" - ); - assert!( - cs.contains("var valueItem = Contact.ReadFrom(valueReader);"), - "async record list elements must decode: {cs}" - ); - // Map results decode from the same single buffer as alternating - // key/value pairs; no parallel buffers remain. - assert!( - cs.contains("var value = new Dictionary(valueCount);") - && cs.contains("var valueKey = valueReader.ReadString();") - && cs.contains("var valueVal = valueReader.ReadI32();"), - "async map result must decode pairs: {cs}" - ); - assert!( - !cs.contains("resultKeys") && !cs.contains("resultValues"), - "parallel map buffers must be gone: {cs}" - ); - // Every owned value buffer is released after the copy. - assert!( - cs.contains("NativeMethods.weaveffi_free_bytes(result, resultLen);"), - "async result buffers are owned and must be freed after copying: {cs}" - ); -} - -/// The iterator contract (`IteratorProtocol`): the sequence streams -/// through a single `yield return` enumerator (one C `next` per -/// `MoveNext`), frees each string element after conversion, destroys the -/// native iterator exactly once from the compiler-generated `finally`, -/// and refuses a second enumeration instead of double-destroying. -#[test] -fn iterator_streams_lazily_and_destroys_once() { - let cs = render_csharp( - &kv_api(), - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - // The single-use wrapper class and the wrapped return. - assert!( - cs.contains("internal sealed class WeaveFFIOnceEnumerable : IEnumerable"), - "once-enumerable class missing: {cs}" - ); - assert!( - cs.contains("return new WeaveFFIOnceEnumerable(EnumerateListKeys(iter));"), - "iterator wrapper must return the once-enumerable: {cs}" - ); - assert!( - cs.contains("this sequence can be enumerated only once"), - "second enumeration must throw: {cs}" - ); - // One C next call per MoveNext, inside a lazy yield-return method. - assert_eq!( - cs.matches("weaveffi_kv_Store_ListKeysIterator_next(iter, out var out_item, ref iterErr)") - .count(), - 1, - "exactly one next call site expected: {cs}" - ); - assert!( - cs.contains("yield return item;"), - "enumerator must stream lazily: {cs}" - ); - // Each yielded string is freed after conversion (ElemFree::String). - assert!( - cs.contains("NativeMethods.weaveffi_free_string(out_item);"), - "string elements must be freed: {cs}" - ); - // Destroy exactly once, from the enumerator's finally (which C#'s - // foreach reaches through Dispose() on early abandonment too). - assert_eq!( - cs.matches("NativeMethods.weaveffi_kv_Store_ListKeysIterator_destroy(iter);") - .count(), - 1, - "exactly one destroy call site expected: {cs}" - ); - assert!(cs.contains("finally"), "destroy must run in finally: {cs}"); -} - -/// A list-of-strings return arrives as one value buffer: the strings -/// decode from the copy and the producer buffer is released exactly once -/// with `weaveffi_free_bytes`; no per-element frees remain. -#[test] -fn string_list_return_decodes_and_frees_buffer_once() { - let api = make_api(vec![simple_module(vec![Function { - name: "names".into(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::StringUtf8))), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }])]); - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("var value = new string[valueCount];") - && cs.contains("var valueItem = valueReader.ReadString();"), - "string elements must decode from the buffer: {cs}" - ); - assert!( - cs.contains("NativeMethods.weaveffi_free_bytes(result, outLen);"), - "value buffer must be released: {cs}" - ); - let names = cs.find("static string[] Names()").expect("Names wrapper"); - assert!( - !cs[names..].contains("weaveffi_free_string("), - "no per-element frees may remain in the wrapper: {cs}" - ); -} - -#[test] -fn dotnet_nested_module_output() { - let api = make_api(vec![Module { - name: "parent".to_string(), - functions: vec![Function { - name: "outer_fn".to_string(), - params: vec![], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![Module { - name: "child".to_string(), - functions: vec![Function { - name: "inner_fn".to_string(), - params: vec![], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }], - }]); - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("public static class Parent"), - "top-level wrapper class missing: {cs}" - ); - assert!( - cs.contains("public static class ParentChild"), - "submodule wrapper class must be flattened to its full path: {cs}" - ); - assert!( - cs.contains("weaveffi_parent_outer_fn"), - "parent P/Invoke missing: {cs}" - ); - assert!( - cs.contains("weaveffi_parent_child_inner_fn"), - "nested child P/Invoke missing: {cs}" - ); -} - -#[test] -fn deprecated_function_generates_annotation() { - let api = make_api(vec![simple_module(vec![Function { - name: "add_old".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: Some("Use AddV2 instead".into()), - since: Some("0.1.0".into()), - }])]); - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("[Obsolete(\"Use AddV2 instead\")]"), - "missing Obsolete attribute: {cs}" - ); -} - -fn doc_api() -> ResolvedApi { - make_api(vec![Module { - name: "docs".into(), - functions: vec![Function { - name: "do_thing".into(), - params: vec![Param { - name: "x".into(), - ty: TypeRef::I32, - mutable: false, - doc: Some("the input value".into()), - }], - returns: Some(TypeRef::I32), - doc: Some("Performs a thing.".into()), - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Item".into(), - doc: Some("An item we track.".into()), - fields: vec![StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: Some("Stable id".into()), - }], - }], - enums: vec![EnumDef { - name: "Kind".into(), - doc: Some("Kind of item.".into()), - variants: vec![EnumVariant { - name: "Small".into(), - value: 0, - doc: Some("A small one".into()), - fields: vec![], - }], - }], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]) -} - -#[test] -fn dotnet_emits_doc_on_function() { - let cs = render_csharp( - &doc_api(), - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("/// Performs a thing."), - "{cs}" - ); -} - -#[test] -fn dotnet_emits_doc_on_struct() { - let cs = render_csharp( - &doc_api(), - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("/// An item we track."), - "{cs}" - ); -} - -#[test] -fn dotnet_emits_doc_on_enum_variant() { - let cs = render_csharp( - &doc_api(), - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!(cs.contains("/// Kind of item."), "{cs}"); - assert!(cs.contains("/// A small one"), "{cs}"); -} - -#[test] -fn dotnet_emits_doc_on_field() { - let cs = render_csharp( - &doc_api(), - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!(cs.contains("/// Stable id"), "{cs}"); -} - -#[test] -fn dotnet_emits_doc_on_param() { - let cs = render_csharp( - &doc_api(), - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("/// the input value"), - "{cs}" - ); -} - -#[test] -fn dotnet_custom_prefix_threads_to_user_symbols() { - let api = make_api(vec![simple_module(vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }])]); - - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "myffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - - // User symbols pick up the configured ABI prefix... - assert!( - cs.contains("myffi_math_add"), - "user symbol must honor the custom prefix: {cs}" - ); - assert!( - !cs.contains("weaveffi_math_add"), - "user symbol must not retain the default prefix: {cs}" - ); - // ...while runtime ABI helpers stay literally `weaveffi_*`. - assert!( - cs.contains("weaveffi_free_string"), - "runtime ABI helper must stay literal: {cs}" - ); -} - -fn shapes_api() -> ResolvedApi { - let shape = EnumDef { - name: "Shape".into(), - doc: Some("An algebraic shape".into()), - variants: vec![ - EnumVariant { - name: "Empty".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Circle".into(), - value: 1, - doc: None, - fields: vec![StructField { - name: "radius".into(), - ty: TypeRef::F64, - doc: None, - }], - }, - EnumVariant { - name: "Rectangle".into(), - value: 2, - doc: None, - fields: vec![ - StructField { - name: "width".into(), - ty: TypeRef::F32, - doc: None, - }, - StructField { - name: "height".into(), - ty: TypeRef::F32, - doc: None, - }, - ], - }, - EnumVariant { - name: "Labeled".into(), - value: 3, - doc: None, - fields: vec![ - StructField { - name: "label".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "count".into(), - ty: TypeRef::U8, - doc: None, - }, - ], - }, - ], - }; - let channel = EnumDef { - name: "Channel".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }; - make_api(vec![Module { - name: "shapes".into(), - functions: vec![ - Function { - name: "describe".into(), - params: vec![Param { - name: "shape".into(), - ty: TypeRef::RichEnum("Shape".into()), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }, - Function { - name: "scale".into(), - params: vec![ - Param { - name: "shape".into(), - ty: TypeRef::RichEnum("Shape".into()), - mutable: false, - doc: None, - }, - Param { - name: "factor".into(), - ty: TypeRef::F64, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::RichEnum("Shape".into())), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }, - Function { - name: "sum_bytes".into(), - params: vec![Param { - name: "values".into(), - ty: TypeRef::List(Box::new(TypeRef::U8)), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::U64), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }, - ], - structs: vec![], - enums: vec![shape, channel], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]) -} - -#[test] -fn rich_enum_generates_sum_type() { - let cs = render_csharp( - &shapes_api(), - "Shapes", - false, - "weaveffi", - "shapes.yml", - "Shapes.cs", - ); - - // Rich enum becomes an abstract sum type, not a C# enum and not a - // disposable handle wrapper. - assert!( - cs.contains("public abstract class Shape"), - "rich enum must be an abstract class: {cs}" - ); - assert!( - !cs.contains("public enum Shape"), - "rich enum must not be a plain enum: {cs}" - ); - assert!( - !cs.contains("Shape : IDisposable"), - "rich enum must not be disposable: {cs}" - ); - // Plain enum is still a value enum. - assert!( - cs.contains("public enum Channel"), - "plain enum must stay an enum: {cs}" - ); - - // One nested sealed class per variant, with typed properties and - // positional constructors instead of factories and getters. - assert!( - cs.contains("public sealed class Empty : Shape"), - "Empty variant class: {cs}" - ); - assert!( - cs.contains("public sealed class Circle : Shape") - && cs.contains("public Circle(double radius)") - && cs.contains("public double Radius { get; }"), - "Circle variant class: {cs}" - ); - assert!( - cs.contains("public sealed class Rectangle : Shape") - && cs.contains("public Rectangle(float width, float height)") - && cs.contains("public float Width { get; }") - && cs.contains("public float Height { get; }"), - "Rectangle variant class: {cs}" - ); - assert!( - cs.contains("public sealed class Labeled : Shape") - && cs.contains("public Labeled(string label, byte count)") - && cs.contains("public string Label { get; }") - && cs.contains("public byte Count { get; }"), - "Labeled variant class: {cs}" - ); - - // The pack pair writes the i32 tag then the active variant's fields. - assert!( - cs.contains("internal void WriteTo(WeaveFFIBufferWriter writer)") - && cs.contains("case Circle v:") - && cs.contains("writer.WriteI32(1);") - && cs.contains("writer.WriteF64(v.Radius);"), - "tag-dispatched encode: {cs}" - ); - assert!( - cs.contains("internal static Shape ReadFrom(WeaveFFIBufferReader reader)") - && cs.contains("var tag = reader.ReadI32();") - && cs.contains("return new Empty();") - && cs.contains("return new Labeled(fLabel, fCount);"), - "tag-dispatched decode: {cs}" - ); - - // Rich enums declare no C symbols at all. - assert!( - !cs.contains("weaveffi_shapes_Shape_"), - "rich enum C symbols must be gone: {cs}" - ); - - // Functions taking the enum pack it into a pinned value buffer and - // pass (ptr, len); returns decode from the freed-after-copy buffer. - assert!( - cs.contains("public static string ShapesDescribe(Shape shape)") - && cs.contains("shape.WriteTo(shapeWriter);") - && cs.contains( - "weaveffi_shapes_describe(shapePin.AddrOfPinnedObject(), (UIntPtr)shapeBuf.Length, ref err)" - ), - "describe via buffered param: {cs}" - ); - assert!( - cs.contains("public static Shape ShapesScale(Shape shape, double factor)") - && cs.contains("var value = Shape.ReadFrom(valueReader);"), - "scale via buffered return: {cs}" - ); - // Numerics smoke: list in, u64 out (plain function path). - assert!( - cs.contains("public static ulong ShapesSumBytes(byte[] values)"), - "sum_bytes wrapper: {cs}" - ); -} - -/// A `kv` module exercising the 0.5.0 surface: a declared error domain, a -/// `Store` interface (real ctor, named factory, sync/iterator/async -/// methods, a static), and free functions with mixed `throws`. -fn kv_api() -> ResolvedApi { - use weaveffi_ir::ir::{ErrorCode, ErrorDomain, InterfaceDef}; - let f = |name: &str, - params: Vec, - returns: Option, - throws: bool, - is_async: bool, - cancellable: bool| Function { - name: name.into(), - params, - returns, - doc: None, - throws, - r#async: is_async, - cancellable, - deprecated: None, - since: None, - }; - let p = |name: &str, ty: TypeRef| Param { - name: name.into(), - ty, - mutable: false, - doc: None, - }; - make_api(vec![Module { - name: "kv".into(), - functions: vec![ - f( - "lookup_store", - vec![p("store", TypeRef::Interface("Store".into()))], - Some(TypeRef::U64), - true, - false, - false, - ), - f("ping", vec![], Some(TypeRef::Bool), false, false, false), - ], - interfaces: vec![InterfaceDef { - name: "Store".into(), - doc: Some("A key-value store.".into()), - constructors: vec![ - f( - "new", - vec![p("path", TypeRef::StringUtf8)], - None, - true, - false, - false, - ), - f( - "open_readonly", - vec![p("path", TypeRef::StringUtf8)], - None, - false, - false, - false, - ), - ], - methods: vec![ - f( - "get", - vec![p("store_key", TypeRef::StringUtf8)], - Some(TypeRef::StringUtf8), - true, - false, - false, - ), - f("count", vec![], Some(TypeRef::U64), false, false, false), - f( - "list_keys", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - true, - false, - false, - ), - f("compact", vec![], None, true, true, true), - ], - statics: vec![f( - "default_capacity", - vec![], - Some(TypeRef::U32), - false, - false, - false, - )], - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: Some(ErrorDomain { - name: "KvError".into(), - codes: vec![ - ErrorCode { - name: "KEY_NOT_FOUND".into(), - code: 1001, - message: "Key not found".into(), - doc: None, - // Structured payload: the missing key and the attempt - // count, exercising the payload decode in FromCode. - fields: vec![ - StructField { - name: "key".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "attempts".into(), - ty: TypeRef::I32, - doc: None, - }, - ], - }, - ErrorCode { - name: "IO_ERROR".into(), - code: 1004, - message: "I/O failure".into(), - doc: Some("Underlying storage failed.".into()), - fields: vec![], - }, - ], - }), - modules: vec![], - }]) -} - -#[test] -fn typed_exception_rendering() { - let cs = render_csharp( - &kv_api(), - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - // The domain exception extends the generic brand exception and drops - // the doubled suffix (KvException, not KvErrorException). - assert!( - cs.contains("public class KvException : WeaveFFIException"), - "typed exception class missing: {cs}" - ); - assert!( - !cs.contains("KvErrorException"), - "doubled suffix must not appear: {cs}" - ); - // Codes surface as PascalCase constants with their ABI values. - assert!( - cs.contains("public const int KeyNotFound = 1001;"), - "code constant missing: {cs}" - ); - assert!( - cs.contains("public const int IoError = 1004;"), - "code constant missing: {cs}" - ); - // FromCode maps known codes to the typed exception and falls back to - // the generic exception for unknown codes, with the default message - // filling an empty slot message. - assert!( - cs.contains( - "internal static WeaveFFIException FromCode(int code, string message, byte[]? payload)" - ), - "FromCode factory missing: {cs}" - ); - // A code with payload fields decodes them into the exception's Data - // dictionary in declaration order. - assert!( - cs.contains("case KeyNotFound:") - && cs.contains( - "var ex = new KvException(code, string.IsNullOrEmpty(message) ? \"Key not found\" : message);" - ), - "typed mapping missing: {cs}" - ); - assert!( - cs.contains("var reader = new WeaveFFIBufferReader(payload);") - && cs.contains("var fKey = reader.ReadString();") - && cs.contains("ex.Data[\"key\"] = fKey;") - && cs.contains("var fAttempts = reader.ReadI32();") - && cs.contains("ex.Data[\"attempts\"] = fAttempts;"), - "payload field decode missing: {cs}" - ); - // A code without fields maps directly. - assert!( - cs.contains("case IoError:") - && cs.contains( - "return new KvException(code, string.IsNullOrEmpty(message) ? \"I/O failure\" : message);" - ), - "fieldless mapping missing: {cs}" - ); - assert!( - cs.contains("default:") && cs.contains("return new WeaveFFIException(code, message);"), - "generic fallback missing: {cs}" - ); - // The error-check helper gains a per-domain variant that copies the - // payload, clears the slot, and throws through FromCode. - assert!( - cs.contains("internal static void CheckKv(WeaveFFIError err)") - && cs.contains("var payload = CopyPayload(err);") - && cs.contains("NativeMethods.weaveffi_error_clear(ref err);") - && cs.contains("throw KvException.FromCode(code, msg, payload);"), - "per-domain check missing: {cs}" - ); -} - -#[test] -fn interface_class_rendering() { - let cs = render_csharp( - &kv_api(), - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - // Opaque-handle wrapper following the struct pattern. - assert!( - cs.contains("public class Store : IDisposable"), - "interface class missing: {cs}" - ); - assert!( - cs.contains("internal Store(IntPtr handle)"), - "internal handle ctor missing: {cs}" - ); - assert!( - cs.contains("internal IntPtr Handle => _handle;"), - "Handle accessor missing: {cs}" - ); - // The `new` constructor is a real C# constructor assigning _handle. - assert!( - cs.contains("public Store(string path)"), - "real constructor missing: {cs}" - ); - assert!( - cs.contains("_handle = result;"), - "constructor must assign the checked handle: {cs}" - ); - // Other constructors become static factories wrapping the pointer. - assert!( - cs.contains("public static Store OpenReadonly(string path)"), - "factory missing: {cs}" - ); - assert!( - cs.contains("return new Store(result);"), - "factory must wrap the owned pointer: {cs}" - ); - // Instance method: non-static, handle as the leading argument. - assert!( - cs.contains("public string Get(string storeKey)"), - "instance method missing: {cs}" - ); - assert!( - cs.contains("NativeMethods.weaveffi_kv_Store_get(_handle, storeKeyPtr, ref err);"), - "method must pass _handle first: {cs}" - ); - // Static member is a plain static method. - assert!( - cs.contains("public static uint DefaultCapacity()"), - "interface static missing: {cs}" - ); - // Iterator method surfaces as IEnumerable with the handle prepended. - assert!( - cs.contains("public IEnumerable ListKeys()"), - "iterator method missing: {cs}" - ); - assert!( - cs.contains("NativeMethods.weaveffi_kv_Store_list_keys(_handle, ref err);"), - "iterator launch must pass _handle: {cs}" - ); - // Async method returns Task and passes the handle to the launcher. - assert!( - cs.contains("public async Task Compact()"), - "async method missing: {cs}" - ); - assert!( - cs.contains( - "NativeMethods.weaveffi_kv_Store_compact_async(_handle, IntPtr.Zero, callback, ctx);" - ), - "async launch must pass _handle and the cancel slot: {cs}" - ); - // Disposal: Dispose + finalizer calling the destroy symbol once. - assert!( - cs.contains("NativeMethods.weaveffi_kv_Store_destroy(_handle);") && cs.contains("~Store()"), - "dispose/finalizer missing: {cs}" - ); - // Externs: destroy plus shape-matched member declarations with the - // implicit self slot on instance members. - for sym in [ - "internal static extern void weaveffi_kv_Store_destroy(IntPtr self);", - "internal static extern IntPtr weaveffi_kv_Store_new(IntPtr path, ref WeaveFFIError err);", - "internal static extern IntPtr weaveffi_kv_Store_open_readonly(IntPtr path, ref WeaveFFIError err);", - "internal static extern IntPtr weaveffi_kv_Store_get(IntPtr self, IntPtr store_key, ref WeaveFFIError err);", - "internal static extern ulong weaveffi_kv_Store_count(IntPtr self, ref WeaveFFIError err);", - "internal static extern IntPtr weaveffi_kv_Store_list_keys(IntPtr self, ref WeaveFFIError out_err);", - "internal static extern int weaveffi_kv_Store_ListKeysIterator_next(", - "internal static extern void weaveffi_kv_Store_ListKeysIterator_destroy(IntPtr iter);", - "internal static extern void weaveffi_kv_Store_compact_async(IntPtr self, IntPtr cancel_token, AsyncCb_weaveffi_kv_Store_compact callback, IntPtr context);", - "internal static extern uint weaveffi_kv_Store_default_capacity(ref WeaveFFIError err);", - ] { - assert!(cs.contains(sym), "missing P/Invoke `{sym}`: {cs}"); - } - // No stray sync extern for the async-only member. - assert!( - !cs.contains("weaveffi_kv_Store_compact(IntPtr self, ref WeaveFFIError err)"), - "async member must not declare a sync extern: {cs}" - ); - // Interface parameters borrow the handle. - assert!( - cs.contains("public static ulong LookupStore(Store store)"), - "interface param wrapper missing: {cs}" - ); - assert!( - cs.contains("NativeMethods.weaveffi_kv_lookup_store(store.Handle, ref err);"), - "interface param must pass obj.Handle: {cs}" - ); -} - -/// Extract the body of the method whose signature contains `sig`, up to -/// the next method boundary (a blank line followed by a doc comment or -/// declaration at the same depth). Good enough to scope error-check -/// assertions to one wrapper. -fn method_slice<'a>(cs: &'a str, sig: &str) -> &'a str { - let start = cs - .find(sig) - .unwrap_or_else(|| panic!("signature `{sig}` not found in: {cs}")); - let rest = &cs[start..]; - let end = rest.find("\n\n").unwrap_or(rest.len()); - &rest[..end] -} - -#[test] -fn throws_split_typed_vs_generic() { - let cs = render_csharp( - &kv_api(), - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - // throws == true: sync method reports through the typed check. - let get = method_slice(&cs, "public string Get(string storeKey)"); - assert!( - get.contains("WeaveFFIError.CheckKv(err);"), - "throwing method must use the typed check: {get}" - ); - // throws == false: generic check only (panics/marshalling). - let count = method_slice(&cs, "public ulong Count()"); - assert!( - count.contains("WeaveFFIError.Check(err);") && !count.contains("CheckKv"), - "non-throwing method must use the generic check: {count}" - ); - // Free functions follow the same split. - let lookup = method_slice(&cs, "public static ulong LookupStore(Store store)"); - assert!( - lookup.contains("WeaveFFIError.CheckKv(err);"), - "throwing free function must use the typed check: {lookup}" - ); - let ping = method_slice(&cs, "public static bool Ping()"); - assert!( - ping.contains("WeaveFFIError.Check(err);") && !ping.contains("CheckKv"), - "non-throwing free function must use the generic check: {ping}" - ); - // The real constructor throws the typed exception too. - let ctor = method_slice(&cs, "public Store(string path)"); - assert!( - ctor.contains("WeaveFFIError.CheckKv(err);"), - "throwing constructor must use the typed check: {ctor}" - ); - // Async completion faults the task with the typed exception; the - // iterator's next-checks are typed as well. - assert!( - cs.contains("var payload = WeaveFFIError.CopyPayload(wErr);") - && cs.contains("tcs.SetException(KvException.FromCode(wErr.Code, msg, payload));"), - "async throws must fault with the typed exception and payload: {cs}" - ); - let iter = method_slice(&cs, "private static IEnumerator EnumerateListKeys("); - assert!( - iter.contains("WeaveFFIError.CheckKv(iterErr);"), - "iterator next-check must be typed: {iter}" - ); - // Throwing wrappers document the exception type. - assert!( - cs.contains( - "/// Thrown when the call reports a KvError code." - ), - "exception doc missing: {cs}" - ); -} - -#[test] -fn wrapper_params_are_camel_case() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "create_contact".into(), - params: vec![ - Param { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: Some("Given name.".into()), - }, - Param { - name: "contact_type".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - let cs = render_csharp( - &api, - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - assert!( - cs.contains("public static int CreateContact(string firstName, int contactType)"), - "wrapper params must be camelCase: {cs}" - ); - assert!( - cs.contains("Marshal.StringToCoTaskMemUTF8(firstName)") - && cs.contains("Marshal.FreeCoTaskMem(firstNamePtr);"), - "marshalling locals must follow the camelCase name: {cs}" - ); - assert!( - cs.contains("/// Given name."), - "param docs must use the camelCase name: {cs}" - ); - // The P/Invoke extern keeps the IDL spelling. - assert!( - cs.contains("internal static extern int weaveffi_contacts_create_contact(IntPtr first_name, int contact_type, ref WeaveFFIError err);"), - "extern must keep IDL parameter names: {cs}" - ); -} - -#[test] -fn default_config_strips_module_prefix() { - let config = DotnetConfig::default(); - assert!( - config.strip_module_prefix, - "strip_module_prefix must default to true" - ); -} - -/// Parse, validate, and render a CLI fixture IDL end to end. Stands in -/// for the CLI-driven generation while `weaveffi-cli` is blocked on other -/// generators mid-overhaul: same parse, validation, model build, and -/// render path the CLI runs, minus the argument plumbing. -fn render_fixture(name: &str) -> String { - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../weaveffi-cli/tests/fixtures") - .join(name); - let text = std::fs::read_to_string(&path) - .unwrap_or_else(|e| panic!("failed to read fixture {}: {e}", path.display())); - let api = weaveffi_ir::parse::parse_api_str(&text, "yml").expect("fixture must parse"); - let api = weaveffi_core::validate::validate_api(api, None).expect("fixture must validate"); - render_csharp(&api, "WeaveFFI", true, "weaveffi", name, "WeaveFFI.cs") -} - -#[test] -fn fixture_contacts_renders_new_surface() { - let cs = render_fixture("02_contacts.yml"); - // Interface class: real ctor for `new`, PascalCase methods with - // camelCase parameters, disposal through the destroy symbol. - assert!( - cs.contains("public class ContactBook : IDisposable"), - "ContactBook class missing: {cs}" - ); - assert!( - cs.contains("public ContactBook()"), - "real constructor missing: {cs}" - ); - assert!( - cs.contains( - "public Contact Add(string firstName, string lastName, string? email, \ - ContactType contactType)" - ), - "Add method missing: {cs}" - ); - assert!( - cs.contains("public Contact Get(long id)"), - "Get method missing: {cs}" - ); - assert!( - cs.contains("public Contact[] List()"), - "List method missing: {cs}" - ); - assert!( - cs.contains("public bool Remove(long id)"), - "Remove method missing: {cs}" - ); - assert!( - cs.contains("public int Count()"), - "Count method missing: {cs}" - ); - assert!( - cs.contains("NativeMethods.weaveffi_contacts_ContactBook_destroy(_handle);") - && cs.contains("~ContactBook()"), - "dispose/finalizer missing: {cs}" - ); - // Typed errors: domain exception with code constants, typed checks in - // throwing methods, generic checks elsewhere. - assert!( - cs.contains("public class ContactsException : WeaveFFIException"), - "ContactsException missing: {cs}" - ); - assert!( - cs.contains("public const int InvalidName = 1;") - && cs.contains("public const int NotFound = 2;"), - "code constants missing: {cs}" - ); - let get = method_slice(&cs, "public Contact Get(long id)"); - assert!( - get.contains("WeaveFFIError.CheckContacts(err);"), - "throwing method must use the typed check: {get}" - ); - let count = method_slice(&cs, "public int Count()"); - assert!( - count.contains("WeaveFFIError.Check(err);") && !count.contains("CheckContacts"), - "non-throwing method must use the generic check: {count}" - ); -} - -#[test] -fn fixture_inventory_renders_two_domains() { - let cs = render_fixture("03_inventory.yml"); - // The products module owns the Catalog interface. - assert!( - cs.contains("public class Catalog : IDisposable"), - "Catalog class missing: {cs}" - ); - assert!( - cs.contains("public Product AddProduct(string name, double price, Category category)"), - "AddProduct method missing: {cs}" - ); - assert!( - cs.contains("public Product GetProduct(long id)"), - "GetProduct method missing: {cs}" - ); - assert!( - cs.contains("NativeMethods.weaveffi_products_Catalog_destroy(_handle);"), - "Catalog destroy missing: {cs}" - ); - // Two error domains, each with its own exception and check helper. - assert!( - cs.contains("public class ProductsException : WeaveFFIException") - && cs.contains("public class OrdersException : WeaveFFIException"), - "both domain exceptions must render: {cs}" - ); - assert!( - cs.contains("public const int InvalidPrice = 1;") - && cs.contains("public const int ProductNotFound = 2;") - && cs.contains("public const int OrderNotFound = 1;") - && cs.contains("public const int EmptyOrder = 2;"), - "per-domain code constants missing: {cs}" - ); - let add = method_slice( - &cs, - "public Product AddProduct(string name, double price, Category category)", - ); - assert!( - add.contains("WeaveFFIError.CheckProducts(err);"), - "Catalog methods must use the products check: {add}" - ); - // The orders module's free functions use their own domain. - let create = method_slice(&cs, "public static long CreateOrder(OrderItem[] items)"); - assert!( - create.contains("WeaveFFIError.CheckOrders(err);"), - "orders functions must use the orders check: {create}" - ); - let cancel = method_slice(&cs, "public static bool CancelOrder(long id)"); - assert!( - cancel.contains("WeaveFFIError.Check(err);") && !cancel.contains("CheckOrders"), - "non-throwing orders function must use the generic check: {cancel}" - ); - // Per-module static classes with stripped method names. - assert!( - cs.contains("public static class Orders"), - "orders wrapper class missing: {cs}" - ); -} - -/// Regression: reserved-word identifiers get the `@` escape everywhere they -/// surface (wrapper params, extern params, derived locals, ctor params, and -/// field assignments). Previously only a partial keyword list was escaped, -/// so `lock`, `foreach`, or `char` produced C# that didn't compile, and a -/// ctor param named `default` silently assigned the `default` literal. -#[test] -fn keyword_identifiers_are_escaped() { - let p = |name: &str, ty: TypeRef| Param { - name: name.into(), - ty, - mutable: false, - doc: None, - }; - let mut m = simple_module(vec![Function { - name: "load".into(), - params: vec![ - p("string", TypeRef::StringUtf8), - p("lock", TypeRef::I32), - p("foreach", TypeRef::Bool), - p("char", TypeRef::Bytes), - p("normal", TypeRef::I64), - ], - returns: Some(TypeRef::I32), - doc: None, - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }]); - m.structs.push(StructDef { - name: "Config".into(), - doc: None, - fields: vec![ - StructField { - name: "default".into(), - ty: TypeRef::I32, - doc: None, - }, - StructField { - name: "normal".into(), - ty: TypeRef::Bool, - doc: None, - }, - ], - }); - let cs = render_csharp( - &make_api(vec![m]), - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - // Wrapper signature: every keyword param carries `@`; normal ones don't. - assert!( - cs.contains( - "public static int Load(string @string, int @lock, bool @foreach, byte[] @char, long normal)" - ), - "wrapper signature must escape keyword params: {cs}" - ); - // The extern declaration escapes direct slots too (`char` lowers to a - // ptr/len pair whose suffixed names aren't keywords). - assert!( - cs.contains("weaveffi_math_load(IntPtr @string, int @lock, byte @foreach, IntPtr char_ptr, UIntPtr char_len, long normal, ref WeaveFFIError err)"), - "extern params must escape keywords: {cs}" - ); - // Locals derived from an escaped param keep the escape. - assert!( - cs.contains("var @charPin = GCHandle.Alloc(@char, GCHandleType.Pinned);"), - "derived pin local must stay escaped: {cs}" - ); - // The record ctor escapes the field param and assigns the param, not - // the C# `default` literal. - assert!( - cs.contains("public Config(int @default, bool normal)"), - "ctor must escape keyword field params: {cs}" - ); - assert!( - cs.contains("Default = @default;"), - "ctor must assign the escaped param, not the default literal: {cs}" - ); -} - -/// Regression: the typed error mapping switches only on the declared -/// (positive) code constants and routes every other value, in particular -/// the negative runtime codes reserved by the ABI (-1 generic, -2 panic, -/// -3 marshalling), to the generic branded exception. -#[test] -fn error_mapping_reserves_negative_codes_for_runtime() { - let cs = render_csharp( - &kv_api(), - "WeaveFFI", - true, - "weaveffi", - "weaveffi.yml", - "WeaveFFI.cs", - ); - // Exactly the two declared codes appear as case arms, and both declared - // constants are positive. - let case_count = cs.matches("case ").count(); - assert_eq!( - case_count, 2, - "FromCode must switch on exactly the declared codes: {cs}" - ); - assert!( - cs.contains("case KeyNotFound:") && cs.contains("case IoError:"), - "declared code arms missing: {cs}" - ); - assert!( - cs.contains("public const int KeyNotFound = 1001;") - && cs.contains("public const int IoError = 1004;"), - "declared constants must keep their positive values: {cs}" - ); - // No literal case arm exists for any negative runtime code; they all - // take the default arm to the generic branded exception. - assert!( - !cs.contains("case -"), - "negative codes must never map to a typed case: {cs}" - ); - assert!( - cs.contains("default:\n return new WeaveFFIException(code, message);"), - "unknown codes must fall back to WeaveFFIException: {cs}" - ); - // Throwing paths in the domain module use the typed check; non-throwing - // paths trap through the generic check. - let lookup = method_slice(&cs, "public static ulong LookupStore(Store store)"); - assert!( - lookup.contains("WeaveFFIError.CheckKv(err);"), - "throwing function must use the typed check: {lookup}" - ); - let ping = method_slice(&cs, "public static bool Ping()"); - assert!( - ping.contains("WeaveFFIError.Check(err);") && !ping.contains("CheckKv"), - "non-throwing function must trap via the generic check: {ping}" - ); -} - -/// Regression: user-provided package metadata routes through `xml_escape` -/// in both manifests, so quotes, apostrophes, and ampersands can't corrupt -/// the XML. Previously the crate-local escaper missed `"` and `'`, and the -/// `.nuspec` project URL wasn't escaped at all. -#[test] -fn manifest_metadata_is_xml_escaped() { - use weaveffi_ir::ir::Package; - let mut api = make_api(vec![simple_module(vec![])]).api().clone(); - api.package = Some(Package { - name: "my-kv".into(), - version: "1.2.3".into(), - description: Some("The \"best\" store & friends, isn't it".into()), - license: Some("MIT OR Apache-2.0".into()), - authors: vec!["Ada \"The First\" ".into()], - homepage: Some("https://example.com/?a=1&b=2".into()), - repository: None, - }); - let api = ResolvedApi::assume_resolved(api); - let config = DotnetConfig::default(); - let package = crate::package::resolve_dotnet_package(&api, &config); - let csproj = crate::package::render_csproj(&package, "weaveffi.yml", "WeaveFFI.csproj"); - assert!( - csproj.contains( - "The "best" store & friends, isn't it" - ), - "csproj description must be XML-escaped: {csproj}" - ); - assert!( - csproj.contains("Ada "The First" <ada@example.com>"), - "csproj authors must be XML-escaped: {csproj}" - ); - assert!( - csproj.contains("https://example.com/?a=1&b=2"), - "csproj project URL must be XML-escaped: {csproj}" - ); - let nuspec = crate::package::render_nuspec(&package, "weaveffi.yml", "WeaveFFI.nuspec"); - assert!( - nuspec.contains( - "The "best" store & friends, isn't it" - ), - "nuspec description must be XML-escaped: {nuspec}" - ); - assert!( - nuspec.contains("https://example.com/?a=1&b=2"), - "nuspec project URL must be XML-escaped: {nuspec}" - ); -} diff --git a/crates/weaveffi-gen-dotnet/src/types.rs b/crates/weaveffi-gen-dotnet/src/types.rs index 01a0513e..457f3baf 100644 --- a/crates/weaveffi-gen-dotnet/src/types.rs +++ b/crates/weaveffi-gen-dotnet/src/types.rs @@ -5,8 +5,8 @@ use heck::ToLowerCamelCase; use weaveffi_core::abi::{AbiParam, CType}; use weaveffi_core::lang; use weaveffi_core::model::FnBinding; +use weaveffi_core::model::Ty; use weaveffi_core::utils::local_type_name; -use weaveffi_ir::ir::TypeRef; /// The C# type of a `handle` reference: a generated `{T}Handle` wrapper /// struct named after the referent's bare local type name. @@ -16,92 +16,90 @@ pub(crate) fn typed_handle_cs(name: &str) -> String { /// The idiomatic C# surface type for one IR type, as it appears in wrapper /// signatures, properties, and locals. -pub(crate) fn cs_type(ty: &TypeRef) -> String { +pub(crate) fn cs_type(ty: &Ty) -> String { match ty { - TypeRef::I8 => "sbyte".into(), - TypeRef::I16 => "short".into(), - TypeRef::I32 => "int".into(), - TypeRef::U8 => "byte".into(), - TypeRef::U16 => "ushort".into(), - TypeRef::U32 => "uint".into(), - TypeRef::I64 => "long".into(), - TypeRef::U64 => "ulong".into(), - TypeRef::F32 => "float".into(), - TypeRef::F64 => "double".into(), - TypeRef::Bool => "bool".into(), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "string".into(), - TypeRef::Handle => "ulong".into(), + Ty::I8 => "sbyte".into(), + Ty::I16 => "short".into(), + Ty::I32 => "int".into(), + Ty::U8 => "byte".into(), + Ty::U16 => "ushort".into(), + Ty::U32 => "uint".into(), + Ty::I64 => "long".into(), + Ty::U64 => "ulong".into(), + Ty::F32 => "float".into(), + Ty::F64 => "double".into(), + Ty::Bool => "bool".into(), + Ty::StringUtf8 | Ty::BorrowedStr => "string".into(), + Ty::Handle => "ulong".into(), // Typed handles surface as a generated `{T}Handle` wrapper struct; a // cross-module referent (e.g. `kv.Token`) uses the bare local name. - TypeRef::TypedHandle(name) => typed_handle_cs(name), - TypeRef::Bytes | TypeRef::BorrowedBytes => "byte[]".into(), + Ty::TypedHandle(name) => typed_handle_cs(name), + Ty::Bytes | Ty::BorrowedBytes => "byte[]".into(), // Records are plain data classes; rich enums are abstract sum types. // Both are value types decoded from value buffers. - TypeRef::Record(name) | TypeRef::RichEnum(name) => local_type_name(name).into(), - TypeRef::Enum(name) => local_type_name(name).into(), - TypeRef::Optional(inner) => match inner.as_ref() { - TypeRef::I8 => "sbyte?".into(), - TypeRef::I16 => "short?".into(), - TypeRef::I32 => "int?".into(), - TypeRef::U8 => "byte?".into(), - TypeRef::U16 => "ushort?".into(), - TypeRef::U32 => "uint?".into(), - TypeRef::I64 => "long?".into(), - TypeRef::U64 => "ulong?".into(), - TypeRef::F32 => "float?".into(), - TypeRef::F64 => "double?".into(), - TypeRef::Bool => "bool?".into(), - TypeRef::Handle => "ulong?".into(), - TypeRef::TypedHandle(name) => format!("{}?", typed_handle_cs(name)), - TypeRef::Enum(name) => format!("{}?", local_type_name(name)), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "string?".into(), - TypeRef::Record(name) | TypeRef::RichEnum(name) => { + Ty::Record(name) | Ty::RichEnum(name) => local_type_name(name).into(), + Ty::Enum(name) => local_type_name(name).into(), + Ty::Optional(inner) => match inner.as_ref() { + Ty::I8 => "sbyte?".into(), + Ty::I16 => "short?".into(), + Ty::I32 => "int?".into(), + Ty::U8 => "byte?".into(), + Ty::U16 => "ushort?".into(), + Ty::U32 => "uint?".into(), + Ty::I64 => "long?".into(), + Ty::U64 => "ulong?".into(), + Ty::F32 => "float?".into(), + Ty::F64 => "double?".into(), + Ty::Bool => "bool?".into(), + Ty::Handle => "ulong?".into(), + Ty::TypedHandle(name) => format!("{}?", typed_handle_cs(name)), + Ty::Enum(name) => format!("{}?", local_type_name(name)), + Ty::StringUtf8 | Ty::BorrowedStr => "string?".into(), + Ty::Record(name) | Ty::RichEnum(name) => { format!("{}?", local_type_name(name)) } _ => format!("{}?", cs_type(inner)), }, - TypeRef::List(inner) => format!("{}[]", cs_type(inner)), - TypeRef::Iterator(inner) => format!("IEnumerable<{}>", cs_type(inner)), - TypeRef::Map(k, v) => format!("Dictionary<{}, {}>", cs_type(k), cs_type(v)), + 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)), // Interfaces surface as their opaque-handle wrapper class; a // cross-module reference (`kv.Store`) uses the bare local name. - TypeRef::Interface(name) => local_type_name(name).into(), - TypeRef::Named(_) => unreachable!("unresolved type reference"), + Ty::Interface(name) => local_type_name(name).into(), } } /// The P/Invoke spelling of one IR type in a delegate or result slot: value /// types pass directly, everything pointer-shaped collapses to `IntPtr`. -pub(crate) fn pinvoke_type(ty: &TypeRef) -> String { +pub(crate) fn pinvoke_type(ty: &Ty) -> String { match ty { - TypeRef::I8 => "sbyte".into(), - TypeRef::I16 => "short".into(), - TypeRef::I32 => "int".into(), - TypeRef::U8 => "byte".into(), - TypeRef::U16 => "ushort".into(), - TypeRef::U32 => "uint".into(), - TypeRef::I64 => "long".into(), - TypeRef::U64 => "ulong".into(), - TypeRef::F32 => "float".into(), - TypeRef::F64 => "double".into(), + Ty::I8 => "sbyte".into(), + Ty::I16 => "short".into(), + Ty::I32 => "int".into(), + Ty::U8 => "byte".into(), + Ty::U16 => "ushort".into(), + Ty::U32 => "uint".into(), + Ty::I64 => "long".into(), + Ty::U64 => "ulong".into(), + Ty::F32 => "float".into(), + Ty::F64 => "double".into(), // C `bool` is one byte; marshalling it as `int` would read past the // slot in arrays and leave garbage in the upper bits of returns. - TypeRef::Bool => "byte".into(), - TypeRef::StringUtf8 - | TypeRef::BorrowedStr - | TypeRef::Bytes - | TypeRef::BorrowedBytes - | TypeRef::Record(_) - | TypeRef::RichEnum(_) - | TypeRef::Interface(_) - | TypeRef::Optional(_) - | TypeRef::List(_) - | TypeRef::Iterator(_) - | TypeRef::Map(_, _) => "IntPtr".into(), - TypeRef::Handle => "ulong".into(), - TypeRef::TypedHandle(_) => "IntPtr".into(), - TypeRef::Enum(_) => "int".into(), - TypeRef::Named(_) => unreachable!("unresolved type reference"), + Ty::Bool => "byte".into(), + Ty::StringUtf8 + | Ty::BorrowedStr + | Ty::Bytes + | Ty::BorrowedBytes + | Ty::Record(_) + | Ty::RichEnum(_) + | Ty::Interface(_) + | Ty::Optional(_) + | Ty::List(_) + | Ty::Iterator(_) + | Ty::Map(_, _) => "IntPtr".into(), + Ty::Handle => "ulong".into(), + Ty::TypedHandle(_) => "IntPtr".into(), + Ty::Enum(_) => "int".into(), } } @@ -147,23 +145,23 @@ pub(crate) fn cs_out_param(p: &AbiParam) -> String { /// `Nullable` and a present value is read through `.Value`. Strings, byte /// arrays, records, rich enums, interfaces, and collections are reference /// types and use plain `null` checks instead. -pub(crate) fn is_cs_value_type(ty: &TypeRef) -> bool { +pub(crate) fn is_cs_value_type(ty: &Ty) -> bool { matches!( ty, - TypeRef::I8 - | TypeRef::I16 - | TypeRef::I32 - | TypeRef::U8 - | TypeRef::U16 - | TypeRef::U32 - | TypeRef::I64 - | TypeRef::U64 - | TypeRef::F32 - | TypeRef::F64 - | TypeRef::Bool - | TypeRef::Handle - | TypeRef::Enum(_) - | TypeRef::TypedHandle(_) + Ty::I8 + | Ty::I16 + | Ty::I32 + | Ty::U8 + | Ty::U16 + | Ty::U32 + | Ty::I64 + | Ty::U64 + | Ty::F32 + | Ty::F64 + | Ty::Bool + | Ty::Handle + | Ty::Enum(_) + | Ty::TypedHandle(_) ) } diff --git a/crates/weaveffi-gen-go/Cargo.toml b/crates/weaveffi-gen-go/Cargo.toml index 17808d61..afbff9d3 100644 --- a/crates/weaveffi-gen-go/Cargo.toml +++ b/crates/weaveffi-gen-go/Cargo.toml @@ -11,7 +11,6 @@ categories = ["development-tools::ffi"] [dependencies] weaveffi-core = { path = "../weaveffi-core", version = "0.21.0" } -weaveffi-ir = { path = "../weaveffi-ir", version = "0.21.0" } camino = { workspace = true } heck = { workspace = true } serde = { workspace = true } diff --git a/crates/weaveffi-gen-go/src/calls.rs b/crates/weaveffi-gen-go/src/calls.rs index 93d6f494..19de60e6 100644 --- a/crates/weaveffi-gen-go/src/calls.rs +++ b/crates/weaveffi-gen-go/src/calls.rs @@ -4,18 +4,18 @@ //! Marshalling dispatch follows the shared plan layer: each parameter's //! passing contract comes from [`ParamBinding::arg_pass`], each result's //! receiving contract from [`plan::ret_pass`], so this module only spells -//! those contracts in Go rather than re-deriving them from `TypeRef`. +//! those contracts in Go rather than re-deriving them from `Ty`. use heck::ToUpperCamelCase; use weaveffi_core::abi::AbiParam; use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ AsyncBinding, BindingModel, CallShape, CallbackBinding, FnBinding, IteratorBinding, ListenerBinding, ModuleBinding, ParamBinding, }; use weaveffi_core::plan::{self, elem_free, ArgPass, ElemFree, ErrorStrategy, RetPass}; use weaveffi_core::utils::{c_abi_struct_name, wrapper_name}; -use weaveffi_ir::ir::TypeRef; use crate::codec::{emit_buffer_read, emit_buffer_write}; use crate::docs::{emit_doc, emit_fn_doc}; @@ -83,7 +83,7 @@ impl<'a> ErrCtx<'a> { /// The Go return-type suffix (including the leading space) of a wrapper /// returning `ret`: `(T, error)`/`error` when throwing, `T`/nothing when /// plain. - fn ret_sig(&self, ret: &Option) -> String { + fn ret_sig(&self, ret: &Option) -> String { match (ret, self.throws) { (Some(r), true) => format!(" ({}, error)", go_type(r)), (Some(r), false) => format!(" {}", go_type(r)), @@ -207,10 +207,10 @@ fn emit_cb_param_arg( ArgPass::Object { slot, nullable } => { let n = &slot.name; if nullable { - let TypeRef::Optional(inner) = &p.ty else { + let Ty::Optional(inner) = &p.ty else { unreachable!("nullable object params are optional interfaces") }; - let TypeRef::Interface(name) = inner.as_ref() else { + let Ty::Interface(name) = inner.as_ref() else { unreachable!("every other optional is buffered") }; let g = go_local(name); @@ -225,12 +225,12 @@ fn emit_cb_param_arg( ArgPass::Direct { slot } => { let n = &slot.name; match &p.ty { - TypeRef::Bool => { + Ty::Bool => { w.line(format!("{arg} := cToBool({n})")); } // A typed handle is a borrowed opaque pointer wrapped // without ownership, like an interface. - TypeRef::TypedHandle(_) => { + Ty::TypedHandle(_) => { w.line(format!("{arg} := {}", go_wrap_expr(&p.ty, n))); } _ => { @@ -369,7 +369,7 @@ fn async_outcome_type(prefix: &str, f: &FnBinding) -> String { /// adopted by the wrapper instead (its `Close` calls the destroy symbol). fn emit_async_result_send( out: &mut String, - ret: &Option, + ret: &Option, outcome: &str, prefix: &str, module: &str, @@ -412,10 +412,10 @@ fn emit_async_result_send( // calls the destroy symbol). RetPass::Object { nullable, .. } => { if nullable { - let TypeRef::Optional(inner) = ty else { + let Ty::Optional(inner) = ty else { unreachable!("nullable object results are optional interfaces") }; - let TypeRef::Interface(name) = inner.as_ref() else { + let Ty::Interface(name) = inner.as_ref() else { unreachable!("every other optional is buffered") }; let g = go_local(name); @@ -432,11 +432,11 @@ fn emit_async_result_send( } } RetPass::Direct => match ty { - TypeRef::Bool => { + Ty::Bool => { w.line(format!("ch <- {outcome}{{val: cToBool(result)}}")); } // A typed handle is a borrowed id wrapped without ownership. - TypeRef::TypedHandle(_) => { + Ty::TypedHandle(_) => { w.line(format!( "ch <- {outcome}{{val: {}}}", go_wrap_expr(ty, "result") @@ -686,12 +686,12 @@ pub(crate) fn render_function( /// Go type of the `out_item` local whose address is passed to an iterator's /// `next` (the C slot is `T*`, so the local is one indirection less). /// Buffered and bytes elements arrive as a `const uint8_t*` buffer pointer. -fn iter_out_item_type(inner: &TypeRef, prefix: &str, module: &str) -> String { +fn iter_out_item_type(inner: &Ty, prefix: &str, module: &str) -> String { match elem_free(inner) { ElemFree::String => "*C.char".into(), ElemFree::Bytes => "*C.uint8_t".into(), ElemFree::None => match inner { - TypeRef::TypedHandle(n) | TypeRef::Interface(n) => { + Ty::TypedHandle(n) | Ty::Interface(n) => { format!("*C.{}", c_abi_struct_name(n, module, prefix)) } _ => c_scalar_type(inner, prefix, module).unwrap_or_else(|| "C.int64_t".into()), @@ -721,20 +721,14 @@ fn indent_block(block: &str) -> String { /// are freed after copying, bytes and buffered elements are copied/decoded /// and released with `weaveffi_free_bytes` (via `wvCopyBuffer`), and by-value /// elements owe nothing. -fn emit_iter_elem_bind( - w: &mut CodeWriter, - inner: &TypeRef, - ef: &ElemFree, - prefix: &str, - module: &str, -) { +fn emit_iter_elem_bind(w: &mut CodeWriter, inner: &Ty, ef: &ElemFree, prefix: &str, module: &str) { match ef { ElemFree::String => { w.line("item := C.GoString(outItem)"); w.line("C.weaveffi_free_string(outItem)"); } ElemFree::Bytes => { - if matches!(inner, TypeRef::Bytes | TypeRef::BorrowedBytes) { + if matches!(inner, Ty::Bytes | Ty::BorrowedBytes) { w.line("item := wvCopyBuffer(outItem, outLen)"); } else { w.line("rItem := &wvReader{buf: wvCopyBuffer(outItem, outLen)}"); @@ -744,12 +738,12 @@ fn emit_iter_elem_bind( } } ElemFree::None => match inner { - TypeRef::Bool => { + Ty::Bool => { w.line("item := cToBool(outItem)"); } // Typed handles and interfaces are opaque pointers the consumer // adopts, even though `elem_free` owes no runtime call for them. - TypeRef::TypedHandle(_) | TypeRef::Interface(_) => { + Ty::TypedHandle(_) | Ty::Interface(_) => { w.line(format!("item := {}", go_wrap_expr(inner, "outItem"))); } _ => { @@ -970,9 +964,9 @@ fn emit_param( } } ArgPass::Direct { .. } => match &p.ty { - TypeRef::Handle => args.push(format!("C.weaveffi_handle_t({name})")), + Ty::Handle => args.push(format!("C.weaveffi_handle_t({name})")), // A typed handle passes its wrapped opaque pointer by value. - TypeRef::TypedHandle(_) => args.push(format!("{name}.ptr")), + Ty::TypedHandle(_) => args.push(format!("{name}.ptr")), _ => args.push(c_scalar_conv(&name, &p.ty, prefix, module)), }, } @@ -985,7 +979,7 @@ fn emit_param( fn emit_return_out_params( pre: &mut String, args: &mut Vec, - ty: &TypeRef, + ty: &Ty, prefix: &str, module: &str, ) { @@ -1007,7 +1001,7 @@ fn emit_return_out_params( /// A buffered return is copied out of the producer-allocated buffer (which /// `wvCopyBuffer` releases with `weaveffi_free_bytes`), decoded, and checked /// for trailing bytes. -fn emit_return(out: &mut String, ty: &TypeRef, prefix: &str, module: &str, tail: &str) { +fn emit_return(out: &mut String, ty: &Ty, prefix: &str, module: &str, tail: &str) { let mut w = CodeWriter::tabs().with_depth(1); match plan::ret_pass(Some(ty), module, prefix) { RetPass::Void => unreachable!("a present return type is never void"), @@ -1035,10 +1029,10 @@ fn emit_return(out: &mut String, ty: &TypeRef, prefix: &str, module: &str, tail: // return means none. RetPass::Object { nullable, .. } => { if nullable { - let TypeRef::Optional(inner) = ty else { + let Ty::Optional(inner) = ty else { unreachable!("nullable object returns are optional interfaces") }; - let TypeRef::Interface(n) = inner.as_ref() else { + let Ty::Interface(n) = inner.as_ref() else { unreachable!("every other optional is buffered") }; let g = go_local(n); @@ -1051,11 +1045,11 @@ fn emit_return(out: &mut String, ty: &TypeRef, prefix: &str, module: &str, tail: } } RetPass::Direct => match ty { - TypeRef::Bool => { + Ty::Bool => { w.line(format!("return cToBool(result){tail}")); } // A typed handle is a borrowed id wrapped without ownership. - TypeRef::TypedHandle(_) => { + Ty::TypedHandle(_) => { w.line(format!("return {}{tail}", go_wrap_expr(ty, "result"))); } _ => { diff --git a/crates/weaveffi-gen-go/src/codec.rs b/crates/weaveffi-gen-go/src/codec.rs index 2a5c765e..ef463779 100644 --- a/crates/weaveffi-gen-go/src/codec.rs +++ b/crates/weaveffi-gen-go/src/codec.rs @@ -4,12 +4,12 @@ //! Both emitters dispatch on the shared [`wire::classify`] classification, //! so the non-obvious folds (handles as `u64` tokens, borrowed views like //! their owned forms, records and rich enums through one user codec) are -//! decided centrally rather than re-derived from `TypeRef` here. +//! decided centrally rather than re-derived from `Ty` here. use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; +use weaveffi_core::model::{Prim, WireType}; use weaveffi_core::utils::c_abi_struct_name; -use weaveffi_core::wire::{self, WireType}; -use weaveffi_ir::ir::TypeRef; use crate::types::{go_local, go_type, handle_wrapper, optional_derefs}; @@ -20,48 +20,48 @@ pub(crate) fn emit_buffer_write( w: &mut CodeWriter, writer: &str, expr: &str, - ty: &TypeRef, + ty: &Ty, site: &str, depth: usize, ) { - match wire::classify(ty) { - WireType::Bool => { + match ty.wire() { + WireType::Prim(Prim::Bool) => { w.line(format!("{writer}.writeBool({expr})")); } - WireType::I8 => { + WireType::Prim(Prim::I8) => { w.line(format!("{writer}.writeI8({expr})")); } - WireType::I16 => { + WireType::Prim(Prim::I16) => { w.line(format!("{writer}.writeI16({expr})")); } - WireType::I32 => { + WireType::Prim(Prim::I32) => { w.line(format!("{writer}.writeI32({expr})")); } - WireType::I64 => { + WireType::Prim(Prim::I64) => { w.line(format!("{writer}.writeI64({expr})")); } - WireType::U8 => { + WireType::Prim(Prim::U8) => { w.line(format!("{writer}.writeU8({expr})")); } - WireType::U16 => { + WireType::Prim(Prim::U16) => { w.line(format!("{writer}.writeU16({expr})")); } - WireType::U32 => { + WireType::Prim(Prim::U32) => { w.line(format!("{writer}.writeU32({expr})")); } - WireType::U64 => { + WireType::Prim(Prim::U64) => { w.line(format!("{writer}.writeU64({expr})")); } - WireType::F32 => { + WireType::Prim(Prim::F32) => { w.line(format!("{writer}.writeF32({expr})")); } - WireType::F64 => { + WireType::Prim(Prim::F64) => { w.line(format!("{writer}.writeF64({expr})")); } // Both handle flavors encode as one u64 token; only the Go-side // representation differs (a bare int64 versus a wrapper pointer). - WireType::Handle => { - if matches!(ty, TypeRef::TypedHandle(_)) { + WireType::Handle(_) => { + if matches!(ty, Ty::TypedHandle(_)) { w.line(format!( "{writer}.writeU64(uint64(uintptr(unsafe.Pointer({expr}.ptr))))" )); @@ -69,10 +69,10 @@ pub(crate) fn emit_buffer_write( w.line(format!("{writer}.writeU64(uint64({expr}))")); } } - WireType::String => { + WireType::Prim(Prim::String) => { w.line(format!("{writer}.writeString({expr})")); } - WireType::Bytes => { + WireType::Prim(Prim::Bytes) => { w.line(format!("{writer}.writeBytes({expr})")); } WireType::Enum(_) => { @@ -125,51 +125,51 @@ pub(crate) fn emit_buffer_read( w: &mut CodeWriter, reader: &str, dst: &str, - ty: &TypeRef, + ty: &Ty, site: &str, depth: usize, prefix: &str, module: &str, ) { - match wire::classify(ty) { - WireType::Bool => { + match ty.wire() { + WireType::Prim(Prim::Bool) => { w.line(format!("{dst} = {reader}.readBool()")); } - WireType::I8 => { + WireType::Prim(Prim::I8) => { w.line(format!("{dst} = {reader}.readI8()")); } - WireType::I16 => { + WireType::Prim(Prim::I16) => { w.line(format!("{dst} = {reader}.readI16()")); } - WireType::I32 => { + WireType::Prim(Prim::I32) => { w.line(format!("{dst} = {reader}.readI32()")); } - WireType::I64 => { + WireType::Prim(Prim::I64) => { w.line(format!("{dst} = {reader}.readI64()")); } - WireType::U8 => { + WireType::Prim(Prim::U8) => { w.line(format!("{dst} = {reader}.readU8()")); } - WireType::U16 => { + WireType::Prim(Prim::U16) => { w.line(format!("{dst} = {reader}.readU16()")); } - WireType::U32 => { + WireType::Prim(Prim::U32) => { w.line(format!("{dst} = {reader}.readU32()")); } - WireType::U64 => { + WireType::Prim(Prim::U64) => { w.line(format!("{dst} = {reader}.readU64()")); } - WireType::F32 => { + WireType::Prim(Prim::F32) => { w.line(format!("{dst} = {reader}.readF32()")); } - WireType::F64 => { + WireType::Prim(Prim::F64) => { w.line(format!("{dst} = {reader}.readF64()")); } // The u64 token decodes back into the Go representation: a bare // int64 for `handle`, a wrapper around the C pointer for // `handle`. - WireType::Handle => { - if let TypeRef::TypedHandle(n) = ty { + WireType::Handle(_) => { + if let Ty::TypedHandle(n) = ty { let g = handle_wrapper(n); let tag = c_abi_struct_name(n, module, prefix); w.line(format!( @@ -179,10 +179,10 @@ pub(crate) fn emit_buffer_read( w.line(format!("{dst} = int64({reader}.readU64())")); } } - WireType::String => { + WireType::Prim(Prim::String) => { w.line(format!("{dst} = {reader}.readString()")); } - WireType::Bytes => { + WireType::Prim(Prim::Bytes) => { w.line(format!("{dst} = {reader}.readBytes()")); } WireType::Enum(n) => { diff --git a/crates/weaveffi-gen-go/src/entities.rs b/crates/weaveffi-gen-go/src/entities.rs index 1f4d9517..5e97a630 100644 --- a/crates/weaveffi-gen-go/src/entities.rs +++ b/crates/weaveffi-gen-go/src/entities.rs @@ -6,12 +6,12 @@ use std::collections::HashSet; use heck::ToUpperCamelCase; use weaveffi_core::codegen::CodeWriter; use weaveffi_core::errors::ERROR_BRAND; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ BindingModel, CallShape, EnumBinding, ErrorBinding, InterfaceBinding, ModuleBinding, StructBinding, }; use weaveffi_core::utils::{c_abi_struct_name, local_type_name}; -use weaveffi_ir::ir::TypeRef; use crate::calls::{render_async_function, render_function, ErrCtx}; use crate::codec::{emit_buffer_read, emit_buffer_write}; @@ -464,23 +464,23 @@ pub(crate) fn render_interface( /// keeps the emitted set deterministic). pub(crate) fn collect_typed_handles(model: &BindingModel, prefix: &str) -> Vec<(String, String)> { fn visit( - ty: &TypeRef, + ty: &Ty, module: &str, prefix: &str, seen: &mut HashSet, out: &mut Vec<(String, String)>, ) { match ty { - TypeRef::TypedHandle(n) => { + Ty::TypedHandle(n) => { let name = handle_wrapper(n); if seen.insert(name.clone()) { out.push((name, c_abi_struct_name(n, module, prefix))); } } - TypeRef::Optional(i) | TypeRef::List(i) | TypeRef::Iterator(i) => { + Ty::Optional(i) | Ty::List(i) | Ty::Iterator(i) => { visit(i, module, prefix, seen, out); } - TypeRef::Map(k, v) => { + Ty::Map(k, v) => { visit(k, module, prefix, seen, out); visit(v, module, prefix, seen, out); } diff --git a/crates/weaveffi-gen-go/src/lib.rs b/crates/weaveffi-gen-go/src/lib.rs index 4b2c3687..ab3c41b6 100644 --- a/crates/weaveffi-gen-go/src/lib.rs +++ b/crates/weaveffi-gen-go/src/lib.rs @@ -20,23 +20,20 @@ mod docs; mod entities; mod package; mod runtime; -#[cfg(test)] -mod tests; mod types; use camino::Utf8Path; use heck::ToUpperCamelCase; use serde::{Deserialize, Serialize}; -use weaveffi_core::abi::is_buffered; use weaveffi_core::backend::{LanguageBackend, OutputFile}; use weaveffi_core::capabilities::TargetCapabilities; +use weaveffi_core::model::Ty; use weaveffi_core::model::{BindingModel, CallShape}; use weaveffi_core::package::{PackageContext, PackagedFile}; use weaveffi_core::pkg; use weaveffi_core::plan::{elem_free, ElemFree}; use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::utils::{render_prelude, render_trailer, wrapper_name, CommentStyle}; -use weaveffi_ir::ir::TypeRef; use crate::calls::{ collect_trampoline_externs, render_async_function, render_callback_trampoline, render_function, @@ -48,7 +45,8 @@ use crate::entities::{ }; use crate::package::{package_files, render_go_mod, render_readme}; use crate::runtime::{ - render_bool_helpers, render_buffer_runtime, render_callback_registry, render_error_infra, + render_abi_version_check, render_bool_helpers, render_buffer_runtime, render_callback_registry, + render_error_infra, }; /// Per-target configuration for [`GoGenerator`]. @@ -168,16 +166,14 @@ impl LanguageBackend for GoGenerator { } } -weaveffi_core::impl_generator_via_backend!(GoGenerator); - // ── Import scanning ── /// `true` when `ty` is a bare bool in a returned position (including an /// iterator element), needing the `cToBool` helper. -fn ret_direct_bool(ty: &TypeRef) -> bool { +fn ret_direct_bool(ty: &Ty) -> bool { match ty { - TypeRef::Bool => true, - TypeRef::Iterator(inner) => matches!(inner.as_ref(), TypeRef::Bool), + Ty::Bool => true, + Ty::Iterator(inner) => matches!(inner.as_ref(), Ty::Bool), _ => false, } } @@ -186,8 +182,6 @@ fn ret_direct_bool(ty: &TypeRef) -> bool { /// the lowered model. #[derive(Default, Clone, Copy)] struct Imports { - /// `fmt` (error formatting); implied by [`err_infra`](Self::err_infra). - fmt: bool, /// `iter` (lazy sequences returned by `iter` functions). iter: bool, /// `unsafe` (pointer staging for strings/bytes/buffers, callback @@ -233,10 +227,10 @@ fn scan_imports(model: &BindingModel) -> Imports { any_callable = true; has_async |= f.is_async; has_iter |= matches!(f.shape, CallShape::Iterator(_)); - buffer_runtime |= f.params.iter().any(|p| is_buffered(&p.ty)); - bool_helpers |= f.params.iter().any(|p| matches!(p.ty, TypeRef::Bool)); + buffer_runtime |= f.params.iter().any(|p| p.ty.is_buffered()); + bool_helpers |= f.params.iter().any(|p| matches!(p.ty, Ty::Bool)); if let Some(ret) = &f.ret { - buffer_runtime |= is_buffered(ret); + buffer_runtime |= ret.is_buffered(); bool_helpers |= ret_direct_bool(ret); } if let CallShape::Iterator(ib) = &f.shape { @@ -246,8 +240,8 @@ fn scan_imports(model: &BindingModel) -> Imports { } for cb in &m.callbacks { any_callbacks = true; - buffer_runtime |= cb.params.iter().any(|p| is_buffered(&p.ty)); - bool_helpers |= cb.params.iter().any(|p| matches!(p.ty, TypeRef::Bool)); + buffer_runtime |= cb.params.iter().any(|p| p.ty.is_buffered()); + bool_helpers |= cb.params.iter().any(|p| matches!(p.ty, Ty::Bool)); } } @@ -260,7 +254,6 @@ fn scan_imports(model: &BindingModel) -> Imports { let unsafe_ptr = err_infra || buffer_runtime || any_callbacks || has_listeners || has_async; Imports { - fmt: err_infra, iter: has_iter, unsafe_ptr, bool_helpers, @@ -314,32 +307,31 @@ pub(crate) fn render_go( out.push_str("*/\n"); out.push_str("import \"C\"\n"); - if imports.fmt || imports.iter || imports.unsafe_ptr || imports.sync || imports.buffer_runtime { - out.push_str("\nimport (\n"); - if imports.buffer_runtime { - out.push_str("\t\"encoding/binary\"\n"); - } - if imports.fmt { - out.push_str("\t\"fmt\"\n"); - } - if imports.iter { - out.push_str("\t\"iter\"\n"); - } - if imports.buffer_runtime { - out.push_str("\t\"math\"\n"); - } - if imports.sync { - out.push_str("\t\"sync\"\n"); - } - if imports.buffer_runtime { - out.push_str("\t\"unicode/utf8\"\n"); - } - if imports.unsafe_ptr { - out.push_str("\t\"unsafe\"\n"); - } - out.push_str(")\n"); + // The ABI check in `init` formats its panic message, so `fmt` is always + // imported; the rest of the block is driven by what the model uses. + out.push_str("\nimport (\n"); + if imports.buffer_runtime { + out.push_str("\t\"encoding/binary\"\n"); } - out.push('\n'); + out.push_str("\t\"fmt\"\n"); + if imports.iter { + out.push_str("\t\"iter\"\n"); + } + if imports.buffer_runtime { + out.push_str("\t\"math\"\n"); + } + if imports.sync { + out.push_str("\t\"sync\"\n"); + } + if imports.buffer_runtime { + out.push_str("\t\"unicode/utf8\"\n"); + } + if imports.unsafe_ptr { + out.push_str("\t\"unsafe\"\n"); + } + out.push_str(")\n\n"); + + render_abi_version_check(&mut out); if imports.bool_helpers { render_bool_helpers(&mut out); diff --git a/crates/weaveffi-gen-go/src/runtime.rs b/crates/weaveffi-gen-go/src/runtime.rs index af8e0ec6..dec8e159 100644 --- a/crates/weaveffi-gen-go/src/runtime.rs +++ b/crates/weaveffi-gen-go/src/runtime.rs @@ -2,9 +2,33 @@ //! error plumbing, the value-buffer reader/writer pair, and the callback //! registry. +use weaveffi_core::cabi::ABI_VERSION; use weaveffi_core::codegen::CodeWriter; use weaveffi_core::errors::ERROR_BRAND; +/// Emit the package `init` that compares the producer's exported +/// `weaveffi_abi_version()` against the revision these bindings were +/// generated for. cgo resolves the symbol at link time, so a producer that +/// predates versioning fails to link; one built for a different revision +/// panics at package initialization with both numbers in the message. +pub(crate) fn render_abi_version_check(out: &mut String) { + let mut w = CodeWriter::tabs(); + w.line("// The ABI revision these bindings were generated against."); + w.line(format!("const wvABIVersion uint32 = {ABI_VERSION}")); + w.blank(); + w.block("func init() {", "}", |w| { + w.block( + "if found := uint32(C.weaveffi_abi_version()); found != wvABIVersion {", + "}", + |w| { + w.line("panic(fmt.Sprintf(\"WeaveFFI ABI mismatch: these bindings expect revision %d but the loaded library reports revision %d\", wvABIVersion, found))"); + }, + ); + }); + w.blank(); + out.push_str(&w.finish()); +} + /// The `boolToC`/`cToBool` pair converting between Go `bool` and cgo's /// `C._Bool`. pub(crate) fn render_bool_helpers(out: &mut String) { diff --git a/crates/weaveffi-gen-go/src/tests.rs b/crates/weaveffi-gen-go/src/tests.rs deleted file mode 100644 index 21841314..00000000 --- a/crates/weaveffi-gen-go/src/tests.rs +++ /dev/null @@ -1,2533 +0,0 @@ -//! Output-content tests for the Go generator: fixtures mirroring the -//! sample IDLs plus assertions over the rendered source. - -use camino::Utf8Path; -use weaveffi_core::codegen::Generator; -use weaveffi_core::errors::ERROR_BRAND; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_ir::ir::{ - Api, CallbackDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, InterfaceDef, - ListenerDef, Module, Param, StructDef, StructField, TypeRef, -}; - -use super::*; - -// ── Fixture helpers ── - -fn api_of(modules: Vec) -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules, - generators: None, - package: None, - }) -} - -fn module(name: &str) -> Module { - Module { - name: name.into(), - functions: vec![], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - } -} - -fn func_of(name: &str, params: Vec, returns: Option) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - } -} - -fn throwing(mut f: Function) -> Function { - f.throws = true; - f -} - -fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - mutable: false, - doc: None, - } -} - -fn field(name: &str, ty: TypeRef) -> StructField { - StructField { - name: name.into(), - ty, - doc: None, - } -} - -fn code(name: &str, value: i32, message: &str) -> ErrorCode { - ErrorCode { - name: name.into(), - code: value, - message: message.into(), - doc: None, - fields: vec![], - } -} - -fn variant(name: &str, value: i32, fields: Vec) -> EnumVariant { - EnumVariant { - name: name.into(), - value, - doc: None, - fields, - } -} - -/// Render with the default surface: `weaveffi` prefix, stripping on. -fn rg(api: &ResolvedApi) -> String { - rg_with(api, "weaveffi", true) -} - -fn rg_with(api: &ResolvedApi, prefix: &str, strip: bool) -> String { - let model = BindingModel::build(api, prefix); - render_go(api, &model, prefix, strip, "weaveffi.yml") -} - -fn calculator_api() -> ResolvedApi { - let mut m = module("calculator"); - m.functions = vec![ - func_of( - "add", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - ), - func_of( - "echo", - vec![param("msg", TypeRef::StringUtf8)], - Some(TypeRef::StringUtf8), - ), - ]; - api_of(vec![m]) -} - -/// Mirrors `samples/kvstore/kvstore.yml`: the `Store` interface (ctor, -/// sync/async/iterator methods, a static), the `KvError` domain, the -/// `Entry` record, the eviction listener, and the nested `kv.stats` -/// submodule taking a cross-module interface parameter. -fn kv_api() -> ResolvedApi { - let mut stats = module("stats"); - stats.structs = vec![StructDef { - name: "Stats".into(), - doc: None, - fields: vec![field("total_entries", TypeRef::I64)], - }]; - stats.functions = vec![throwing(func_of( - "get_stats", - // Cross-module references reach generators pre-qualified by the - // validator's resolve step; mirror that spelling here. - vec![param("store", TypeRef::Interface("kv.Store".into()))], - Some(TypeRef::Record("Stats".into())), - ))]; - - let mut kv = module("kv"); - kv.errors = Some(ErrorDomain { - name: "KvError".into(), - codes: vec![ - code("KeyNotFound", 1001, "key not found"), - code("Expired", 1002, "entry expired"), - code("StoreFull", 1003, "store has reached capacity"), - code("IoError", 1004, "I/O failure"), - ], - }); - kv.structs = vec![StructDef { - name: "Entry".into(), - doc: None, - fields: vec![ - field("id", TypeRef::I64), - field("key", TypeRef::StringUtf8), - field("value", TypeRef::Bytes), - field("expires_at", TypeRef::Optional(Box::new(TypeRef::I64))), - field("tags", TypeRef::List(Box::new(TypeRef::StringUtf8))), - ], - }]; - kv.enums = vec![EnumDef { - name: "EntryKind".into(), - doc: None, - variants: vec![ - variant("Volatile", 0, vec![]), - variant("Persistent", 1, vec![]), - ], - }]; - kv.callbacks = vec![CallbackDef { - name: "OnEvict".into(), - doc: None, - params: vec![param("key", TypeRef::StringUtf8)], - }]; - kv.listeners = vec![ListenerDef { - name: "eviction_listener".into(), - event_callback: "OnEvict".into(), - doc: None, - }]; - kv.interfaces = vec![InterfaceDef { - name: "Store".into(), - doc: Some("An embedded key-value store owning its entries".into()), - constructors: vec![throwing(func_of( - "open", - vec![param("path", TypeRef::StringUtf8)], - None, - ))], - methods: vec![ - throwing(func_of( - "put", - vec![ - param("key", TypeRef::StringUtf8), - param("value", TypeRef::Bytes), - param("kind", TypeRef::Enum("EntryKind".into())), - param("ttl_seconds", TypeRef::Optional(Box::new(TypeRef::I64))), - ], - Some(TypeRef::Bool), - )), - throwing(func_of( - "get", - vec![param("key", TypeRef::StringUtf8)], - Some(TypeRef::Optional(Box::new(TypeRef::Record("Entry".into())))), - )), - throwing(func_of( - "list_keys", - vec![param( - "prefix", - TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - )], - Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - )), - func_of("count", vec![], Some(TypeRef::I64)), - func_of("clear", vec![], None), - { - let mut f = throwing(func_of("compact", vec![], Some(TypeRef::I64))); - f.r#async = true; - f.cancellable = true; - f - }, - { - let mut f = throwing(func_of( - "legacy_put", - vec![ - param("key", TypeRef::StringUtf8), - param("value", TypeRef::Bytes), - ], - Some(TypeRef::Bool), - )); - f.deprecated = Some("use put() with explicit kind".into()); - f - }, - ], - statics: vec![func_of("default_capacity", vec![], Some(TypeRef::I64))], - }]; - kv.modules = vec![stats]; - api_of(vec![kv]) -} - -/// Mirrors `samples/contacts/contacts.yml`, standing in for the CLI test -/// (`cli_go.rs`) while the workspace binary is blocked on other generator -/// crates mid-overhaul. -fn contacts_api() -> ResolvedApi { - let mut m = module("contacts"); - m.enums = vec![EnumDef { - name: "ContactType".into(), - doc: None, - variants: vec![ - variant("Personal", 0, vec![]), - variant("Work", 1, vec![]), - variant("Other", 2, vec![]), - ], - }]; - m.structs = vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - field("id", TypeRef::I64), - field("first_name", TypeRef::StringUtf8), - field("last_name", TypeRef::StringUtf8), - field("email", TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - field("contact_type", TypeRef::Enum("ContactType".into())), - ], - }]; - m.errors = Some(ErrorDomain { - name: "ContactsError".into(), - codes: vec![ - code("InvalidName", 1, "name must not be empty"), - code("NotFound", 2, "contact not found"), - ], - }); - m.interfaces = vec![InterfaceDef { - name: "ContactBook".into(), - doc: Some("An in-memory address book owning its contacts".into()), - constructors: vec![func_of("new", vec![], None)], - methods: vec![ - throwing(func_of( - "add", - vec![ - param("first_name", TypeRef::StringUtf8), - param("last_name", TypeRef::StringUtf8), - param("email", TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - param("contact_type", TypeRef::Enum("ContactType".into())), - ], - Some(TypeRef::Record("Contact".into())), - )), - throwing(func_of( - "get", - vec![param("id", TypeRef::I64)], - Some(TypeRef::Record("Contact".into())), - )), - func_of( - "list", - vec![], - Some(TypeRef::List(Box::new(TypeRef::Record("Contact".into())))), - ), - func_of( - "remove", - vec![param("id", TypeRef::I64)], - Some(TypeRef::Bool), - ), - func_of("count", vec![], Some(TypeRef::I32)), - ], - statics: vec![], - }]; - api_of(vec![m]) -} - -/// A module with one rich (algebraic) enum used across params and -/// returns. -fn shapes_api() -> ResolvedApi { - let mut m = module("shapes"); - m.enums = vec![EnumDef { - name: "Shape".into(), - doc: None, - variants: vec![ - variant("Empty", 0, vec![]), - variant("Circle", 1, vec![field("radius", TypeRef::F64)]), - variant( - "Labeled", - 3, - vec![ - field("label", TypeRef::StringUtf8), - field("count", TypeRef::U8), - ], - ), - ], - }]; - m.functions = vec![ - func_of( - "describe", - vec![param("shape", TypeRef::RichEnum("Shape".into()))], - Some(TypeRef::StringUtf8), - ), - func_of( - "scale", - vec![ - param("shape", TypeRef::RichEnum("Shape".into())), - param("factor", TypeRef::F64), - ], - Some(TypeRef::RichEnum("Shape".into())), - ), - ]; - api_of(vec![m]) -} - -// ── Scaffolding and packaging ── - -#[test] -fn package_rewrites_cgo_and_bundles_libs() { - use weaveffi_core::package::{FileContent, PackageContext}; - use weaveffi_core::platform::{BinarySet, Platform}; - - let api = calculator_api(); - let model = BindingModel::build(&api, "weaveffi"); - let mut bins = BinarySet::new("calculator"); - bins.insert(Platform::MacosArm64, "/s/darwin-arm64/libcalculator.dylib"); - bins.insert(Platform::WindowsX64, "/s/windows-x64/calculator.dll"); - let ctx = PackageContext { - binaries: &bins, - input_basename: Some("calculator.yml"), - }; - // Mirror the CLI: the config basename drives the `-l` link name, - // which must match the bundled library's base name. - let cfg = GoConfig { - input_basename: Some("calculator.yml".into()), - ..GoConfig::default() - }; - let files = LanguageBackend::package( - &GoGenerator, - &api, - &model, - &ctx, - Utf8Path::new("/out"), - &cfg, - ) - .expect("go supports packaging"); - - assert_eq!(files.iter().filter(|f| f.is_binary()).count(), 2); - let go = files - .iter() - .find(|f| f.path.as_str().ends_with("go/weaveffi.go")) - .expect("go source present"); - let FileContent::Text(src) = &go.content else { - panic!("go source is text"); - }; - assert!( - src.contains("#cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/lib/darwin-arm64"), - "cgo preamble not rewritten: {src}" - ); - assert!(src.contains("#cgo windows,amd64 LDFLAGS: -L${SRCDIR}/lib/windows-x64")); - assert!(src.contains("#cgo LDFLAGS: -lcalculator")); -} - -#[test] -fn name_returns_go() { - assert_eq!(Generator::name(&GoGenerator), "go"); -} - -#[test] -fn output_files_correct() { - let api = calculator_api(); - let out = Utf8Path::new("out"); - let files = GoGenerator.output_files(&api, out, &GoConfig::default()); - assert_eq!( - files, - vec![ - format!("{out}/go/README.md"), - format!("{out}/go/go.mod"), - format!("{out}/go/weaveffi.go"), - ] - ); -} - -#[test] -fn package_and_cgo_preamble() { - let go = rg(&calculator_api()); - assert!(go.contains("package weaveffi\n"), "missing package"); - assert!( - go.contains("#cgo LDFLAGS: -lweaveffi"), - "missing LDFLAGS: {go}" - ); - assert!( - go.contains("#include \"weaveffi.h\""), - "missing weaveffi.h include: {go}" - ); - assert!(go.contains("import \"C\""), "missing import C: {go}"); -} - -#[test] -fn imports_fmt_and_unsafe() { - let go = rg(&calculator_api()); - assert!(go.contains("\"fmt\""), "missing fmt import: {go}"); - assert!(go.contains("\"unsafe\""), "missing unsafe import: {go}"); -} - -// ── Plain (non-throwing) functions ── - -#[test] -fn simple_i32_function() { - let go = rg(&calculator_api()); - assert!( - go.contains("func Add(a int32, b int32) int32 {"), - "missing plain function sig: {go}" - ); - assert!( - go.contains("C.weaveffi_calculator_add("), - "missing C call: {go}" - ); - assert!(go.contains("C.int32_t(a)"), "missing param cast: {go}"); - assert!(go.contains("return int32(result)"), "missing return: {go}"); - assert!( - !go.contains("return int32(result), nil"), - "plain function must not return an error: {go}" - ); -} - -#[test] -fn string_function() { - let go = rg(&calculator_api()); - assert!( - go.contains("func Echo(msg string) string {"), - "missing echo sig: {go}" - ); - assert!(go.contains("C.CString(msg)"), "missing CString: {go}"); - assert!( - go.contains("defer C.free(unsafe.Pointer("), - "missing defer free: {go}" - ); - assert!(go.contains("C.GoString(result)"), "missing GoString: {go}"); - assert!( - go.contains("C.weaveffi_free_string(result)"), - "missing free_string: {go}" - ); -} - -#[test] -fn plain_function_traps_on_error() { - let go = rg(&calculator_api()); - assert!( - go.contains("var cErr C.weaveffi_error"), - "missing error var: {go}" - ); - assert!(go.contains("wvTrap(&cErr)"), "missing trap check: {go}"); - assert!( - go.contains("func wvTrap(cErr *C.weaveffi_error) {"), - "missing wvTrap helper: {go}" - ); - assert!( - go.contains("C.weaveffi_error_clear(cErr)"), - "missing error clear in wvTakeError: {go}" - ); - assert!( - go.contains("panic(fmt.Sprintf(\"weaveffi: %s (code %d)\", msg, code))"), - "wvTrap must panic: {go}" - ); -} - -#[test] -fn void_function() { - let mut m = module("system"); - m.functions = vec![func_of("reset", vec![], None)]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("func Reset() {"), - "missing plain void sig: {go}" - ); - assert!( - go.contains("wvTrap(&cErr)"), - "plain void must trap on error: {go}" - ); - assert!( - !go.contains("func Reset() error"), - "plain void must not return error: {go}" - ); -} - -#[test] -fn handle_type() { - let mut m = module("store"); - m.functions = vec![func_of( - "create", - vec![param("name", TypeRef::StringUtf8)], - Some(TypeRef::Handle), - )]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("func Create(name string) int64 {"), - "handle return should be plain int64: {go}" - ); - assert!( - go.contains("return int64(result)"), - "missing handle return conversion: {go}" - ); -} - -#[test] -fn bool_function_generates_helpers() { - let mut m = module("logic"); - m.functions = vec![func_of( - "negate", - vec![param("val", TypeRef::Bool)], - Some(TypeRef::Bool), - )]; - let go = rg(&api_of(vec![m])); - assert!(go.contains("func boolToC("), "missing boolToC: {go}"); - assert!(go.contains("func cToBool("), "missing cToBool: {go}"); - assert!( - go.contains("boolToC(val)"), - "missing boolToC call for param: {go}" - ); - assert!( - go.contains("cToBool(result)"), - "missing cToBool for return: {go}" - ); -} - -#[test] -fn enum_param_and_return() { - let mut m = module("paint"); - m.functions = vec![func_of( - "mix", - vec![param("a", TypeRef::Enum("Color".into()))], - Some(TypeRef::Enum("Color".into())), - )]; - m.enums = vec![EnumDef { - name: "Color".into(), - doc: None, - variants: vec![variant("Red", 0, vec![])], - }]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("func Mix(a Color) Color {"), - "missing enum function sig: {go}" - ); - assert!( - go.contains("C.weaveffi_paint_Color(a)"), - "missing enum param conversion: {go}" - ); - assert!( - go.contains("Color(result)"), - "missing enum return conversion: {go}" - ); -} - -// ── Buffered params and returns ── - -#[test] -fn struct_return_decodes_buffer() { - let mut m = module("contacts"); - m.functions = vec![func_of( - "get_contact", - vec![param("id", TypeRef::Handle)], - Some(TypeRef::Record("Contact".into())), - )]; - m.structs = vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("func GetContact(id int64) Contact {"), - "record return should be a bare value struct: {go}" - ); - assert!( - go.contains("var cOutLen C.size_t"), - "missing out_len slot: {go}" - ); - assert!( - go.contains("rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)}"), - "buffered return must copy then free through wvCopyBuffer: {go}" - ); - assert!( - go.contains("goResult = wvUnpackContact(rRes)"), - "missing record decode: {go}" - ); - assert!( - go.contains("rRes.expectEnd()"), - "decoder must reject trailing bytes: {go}" - ); - assert!( - !go.contains("&Contact{ptr:"), - "records no longer wrap C pointers: {go}" - ); -} - -#[test] -fn buffered_record_param_packs() { - let mut m = module("contacts"); - m.structs = vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }]; - m.functions = vec![func_of( - "save_contact", - vec![param("contact", TypeRef::Record("Contact".into()))], - None, - )]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("func SaveContact(contact Contact) {"), - "record param should be a bare value struct: {go}" - ); - assert!( - go.contains("wContact := &wvWriter{}"), - "missing writer staging: {go}" - ); - assert!( - go.contains("wvPackContact(wContact, contact)"), - "missing record pack call: {go}" - ); - assert!( - go.contains("cContactPtr = (*C.uint8_t)(unsafe.Pointer(&wContact.buf[0]))"), - "missing buffer pointer staging: {go}" - ); - assert!( - go.contains( - "C.weaveffi_contacts_save_contact(cContactPtr, C.size_t(len(wContact.buf)), &cErr)" - ), - "buffered param must pass ptr + len: {go}" - ); -} - -#[test] -fn optional_string_param() { - let mut m = module("store"); - m.functions = vec![func_of( - "find", - vec![param( - "query", - TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - )], - None, - )]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("query *string"), - "optional string param should be *string: {go}" - ); - assert!( - go.contains("if query == nil {"), - "missing nil check for optional: {go}" - ); - assert!( - go.contains("wQuery.writeOptionFlag(false)"), - "missing absent flag write: {go}" - ); - assert!( - go.contains("wQuery.writeString((*query))"), - "missing dereferenced string write: {go}" - ); - assert!( - go.contains("C.size_t(len(wQuery.buf))"), - "optional param must pass the encoded length: {go}" - ); -} - -#[test] -fn optional_struct_return() { - let mut m = module("contacts"); - m.structs = vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }]; - m.functions = vec![func_of( - "find", - vec![param("id", TypeRef::I32)], - Some(TypeRef::Optional(Box::new(TypeRef::Record( - "Contact".into(), - )))), - )]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("func Find(id int32) *Contact {"), - "optional struct return: {go}" - ); - assert!( - go.contains("if rRes.readOptionFlag() {"), - "missing option flag check: {go}" - ); - assert!( - go.contains("oRes0 = wvUnpackContact(rRes)"), - "missing inner decode: {go}" - ); - assert!( - go.contains("goResult = &oRes0"), - "present value must be pointer-wrapped: {go}" - ); -} - -#[test] -fn list_return_decodes_buffer() { - let mut m = module("store"); - m.functions = vec![func_of( - "list_ids", - vec![], - Some(TypeRef::List(Box::new(TypeRef::I32))), - )]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("func ListIds() []int32 {"), - "missing plain list return sig: {go}" - ); - assert!( - go.contains("var cOutLen C.size_t"), - "missing out_len var: {go}" - ); - assert!( - go.contains("nRes0 := rRes.readLen()"), - "missing count read: {go}" - ); - assert!( - go.contains("goResult = make([]int32, nRes0)"), - "missing slice allocation: {go}" - ); - assert!( - go.contains("goResult[iRes0] = rRes.readI32()"), - "missing element decode: {go}" - ); -} - -#[test] -fn struct_list_return_decodes_elements() { - let mut m = module("contacts"); - m.functions = vec![func_of( - "list_contacts", - vec![], - Some(TypeRef::List(Box::new(TypeRef::Record("Contact".into())))), - )]; - m.structs = vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("func ListContacts() []Contact {"), - "record lists hold values, not pointers: {go}" - ); - assert!( - go.contains("goResult[iRes0] = wvUnpackContact(rRes)"), - "missing per-element record decode: {go}" - ); -} - -#[test] -fn optional_i32_param_and_return() { - let mut m = module("store"); - m.functions = vec![func_of( - "find", - vec![param("id", TypeRef::Optional(Box::new(TypeRef::I32)))], - Some(TypeRef::Optional(Box::new(TypeRef::I32))), - )]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("id *int32"), - "optional i32 param should be *int32: {go}" - ); - assert!( - go.contains("wId.writeI32((*id))"), - "missing dereferenced scalar write: {go}" - ); - assert!( - go.contains("var goResult *int32"), - "optional i32 return should be *int32: {go}" - ); - assert!( - go.contains("oRes0 = rRes.readI32()"), - "missing scalar decode: {go}" - ); -} - -#[test] -fn map_return_decodes_buffer() { - let mut m = module("store"); - m.functions = vec![func_of( - "counts", - vec![], - Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )), - )]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("func Counts() map[string]int32 {"), - "missing map return sig: {go}" - ); - assert!( - go.contains("goResult = make(map[string]int32, nRes0)"), - "missing map allocation: {go}" - ); - assert!( - go.contains("kRes0 = rRes.readString()"), - "missing key decode: {go}" - ); - assert!( - go.contains("vRes0 = rRes.readI32()"), - "missing value decode: {go}" - ); - assert!( - go.contains("goResult[kRes0] = vRes0"), - "missing map insert: {go}" - ); -} - -#[test] -fn map_param_packs() { - let mut m = module("metrics"); - m.functions = vec![func_of( - "record_counts", - vec![param( - "counts", - TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - )], - None, - )]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("func RecordCounts(counts map[string]int32) {"), - "missing map param sig: {go}" - ); - assert!( - go.contains("wCounts.writeLen(len(counts))"), - "missing count write: {go}" - ); - assert!( - go.contains("for kCounts0, vCounts0 := range counts {"), - "missing pair loop: {go}" - ); - assert!( - go.contains("wCounts.writeString(kCounts0)"), - "missing key write: {go}" - ); - assert!( - go.contains("wCounts.writeI32(vCounts0)"), - "missing value write: {go}" - ); -} - -#[test] -fn optional_scalar_return_decodes_buffer() { - let mut m = module("store"); - m.functions = vec![func_of( - "capacity", - vec![], - Some(TypeRef::Optional(Box::new(TypeRef::I64))), - )]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("func Capacity() *int64 {"), - "optional scalar return should be a pointer: {go}" - ); - assert!( - go.contains("wvCopyBuffer(result, cOutLen)"), - "buffered return must copy then free: {go}" - ); - assert!( - go.contains("oRes0 = rRes.readI64()"), - "missing scalar decode: {go}" - ); - assert!(go.contains("\t\"unsafe\"\n"), "unsafe import needed: {go}"); -} - -// ── Throwing functions ── - -fn store_api() -> ResolvedApi { - let mut m = module("store"); - m.errors = Some(ErrorDomain { - name: "StoreError".into(), - codes: vec![code("SaveFailed", 1, "save failed")], - }); - m.functions = vec![ - throwing(func_of( - "save", - vec![param("data", TypeRef::StringUtf8)], - Some(TypeRef::I32), - )), - throwing(func_of("flush", vec![], None)), - func_of("clear", vec![], None), - ]; - api_of(vec![m]) -} - -#[test] -fn throws_split_sync() { - let go = rg(&store_api()); - // throws == true keeps `(T, error)` and maps through the domain. - assert!( - go.contains("func Save(data string) (int32, error) {"), - "missing throwing sig: {go}" - ); - assert!( - go.contains("if cErr.code != 0 {"), - "missing error check: {go}" - ); - assert!( - go.contains("return 0, wvMapStore(wvTakeError(&cErr))"), - "throwing wrapper must map the domain error: {go}" - ); - assert!( - go.contains("return int32(result), nil"), - "throwing wrapper must return `, nil` on success: {go}" - ); - // Throwing void: `error` result, nil on success. - assert!( - go.contains("func Flush() error {"), - "missing throwing void sig: {go}" - ); - assert!( - go.contains("return wvMapStore(wvTakeError(&cErr))"), - "throwing void must return the mapped error: {go}" - ); - assert!(go.contains("return nil"), "missing nil return: {go}"); - // throws == false stays plain and traps. - assert!( - go.contains("func Clear() {"), - "missing plain void sig: {go}" - ); - assert!(go.contains("wvTrap(&cErr)"), "missing trap: {go}"); -} - -#[test] -fn typed_error_surface() { - let go = rg(&store_api()); - assert!( - go.contains("type StoreError struct {"), - "missing typed error struct: {go}" - ); - assert!( - go.contains("func (e *StoreError) Error() string {"), - "typed error must implement error: {go}" - ); - assert!( - go.contains("StoreErrorSaveFailed int32 = 1"), - "missing exported code constant: {go}" - ); - assert!( - go.contains("func wvMapStore(code int32, message string, payload []byte) error {"), - "missing domain mapping helper: {go}" - ); - assert!( - go.contains("message = \"save failed\""), - "missing default message fill: {go}" - ); - assert!( - go.contains("return wvBrandError(code, message, payload)"), - "unknown codes must fall back to the brand error: {go}" - ); - assert!( - go.contains(&format!("type {ERROR_BRAND} struct {{")), - "missing generic brand error: {go}" - ); -} - -#[test] -fn wv_take_error_returns_payload() { - let go = rg(&store_api()); - assert!( - go.contains("func wvTakeError(cErr *C.weaveffi_error) (int32, string, []byte) {"), - "wvTakeError must return the payload triple: {go}" - ); - assert!( - go.contains( - "payload = C.GoBytes(unsafe.Pointer(cErr.payload_ptr), C.int(cErr.payload_len))" - ), - "wvTakeError must copy the payload before clearing: {go}" - ); - assert!( - go.contains("code, msg, _ := wvTakeError(cErr)"), - "wvTrap discards the payload: {go}" - ); -} - -#[test] -fn error_payload_fields_decode() { - let mut m = module("store"); - m.errors = Some(ErrorDomain { - name: "StoreError".into(), - codes: vec![ - code("SaveFailed", 1, "save failed"), - ErrorCode { - name: "Conflict".into(), - code: 2, - message: "write conflict".into(), - doc: None, - fields: vec![ - field("key", TypeRef::StringUtf8), - field("attempts", TypeRef::I32), - ], - }, - ], - }); - m.functions = vec![throwing(func_of( - "save", - vec![param("data", TypeRef::StringUtf8)], - None, - ))]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("Payload any"), - "domain with payload codes must expose Payload: {go}" - ); - assert!( - go.contains("type StoreErrorConflictPayload struct {"), - "missing per-code payload struct: {go}" - ); - assert!( - go.contains("Key string") && go.contains("Attempts int32"), - "payload struct must carry the declared fields: {go}" - ); - assert!( - go.contains("p.Key = r.readString()") && go.contains("p.Attempts = r.readI32()"), - "payload fields must decode in wire order: {go}" - ); - assert!( - go.contains("e.Payload = p"), - "decoded payload must attach to the error: {go}" - ); - assert!( - go.contains("r.expectEnd()"), - "payload decode must reject trailing bytes: {go}" - ); - // A code without fields keeps the simple construction. - assert!( - go.contains("return &StoreError{Code: code, Message: message}"), - "codes without fields skip payload plumbing: {go}" - ); -} - -// ── Enums, records, rich enums ── - -#[test] -fn enum_generation() { - let mut m = module("paint"); - m.enums = vec![EnumDef { - name: "Color".into(), - doc: None, - variants: vec![ - variant("Red", 0, vec![]), - variant("Green", 1, vec![]), - variant("Blue", 2, vec![]), - ], - }]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("type Color int32"), - "missing enum typedef: {go}" - ); - assert!( - go.contains("ColorRed Color = 0"), - "missing Red variant: {go}" - ); - assert!( - go.contains("ColorGreen Color = 1"), - "missing Green variant: {go}" - ); - assert!( - go.contains("ColorBlue Color = 2"), - "missing Blue variant: {go}" - ); -} - -#[test] -fn record_is_plain_value_struct() { - let mut m = module("contacts"); - m.structs = vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - field("name", TypeRef::StringUtf8), - field("age", TypeRef::I32), - ], - }]; - let go = rg(&api_of(vec![m])); - assert!(go.contains("type Contact struct {"), "missing struct: {go}"); - assert!( - go.contains("\tName string\n"), - "missing typed Name field: {go}" - ); - assert!( - go.contains("\tAge int32\n"), - "missing typed Age field: {go}" - ); - assert!( - go.contains("func wvPackContact(w *wvWriter, v Contact) {"), - "missing pack function: {go}" - ); - assert!( - go.contains("w.writeString(v.Name)") && go.contains("w.writeI32(v.Age)"), - "pack must serialize fields in order: {go}" - ); - assert!( - go.contains("func wvUnpackContact(r *wvReader) Contact {"), - "missing unpack function: {go}" - ); - assert!( - go.contains("v.Name = r.readString()") && go.contains("v.Age = r.readI32()"), - "unpack must decode fields in order: {go}" - ); - // Records have no C symbols: no handle wrapping, no destroy, no - // getters, no builders. - assert!( - !go.contains("ptr *C.weaveffi_contacts_Contact"), - "records must not wrap a C pointer: {go}" - ); - assert!( - !go.contains("Contact_destroy") && !go.contains("func (s *Contact)"), - "records have no destroy or getters: {go}" - ); - assert!( - !go.contains("ContactBuilder"), - "records have no builders: {go}" - ); -} - -#[test] -fn record_optional_string_field() { - let mut m = module("contacts"); - m.structs = vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field( - "email", - TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - )], - }]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("\tEmail *string\n"), - "optional string field should be *string: {go}" - ); - assert!( - go.contains("if v.Email == nil {"), - "pack must branch on presence: {go}" - ); - assert!( - go.contains("w.writeString((*v.Email))"), - "pack must dereference the present value: {go}" - ); - assert!( - go.contains("oEmail0 = r.readString()") && go.contains("v.Email = &oEmail0"), - "unpack must pointer-wrap the present value: {go}" - ); -} - -#[test] -fn record_bytes_field_roundtrips() { - let mut m = module("contacts"); - m.structs = vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - field("name", TypeRef::StringUtf8), - field("photo", TypeRef::Bytes), - ], - }]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("w.writeBytes(v.Photo)"), - "bytes fields pack as length-prefixed buffers: {go}" - ); - assert!( - go.contains("v.Photo = r.readBytes()"), - "bytes fields decode as copies: {go}" - ); -} - -#[test] -fn record_enum_field() { - let mut m = module("contacts"); - m.structs = vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("contact_type", TypeRef::Enum("ContactType".into()))], - }]; - m.enums = vec![EnumDef { - name: "ContactType".into(), - doc: None, - variants: vec![variant("Personal", 0, vec![])], - }]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("\tContactType ContactType\n"), - "missing enum-typed field: {go}" - ); - assert!( - go.contains("w.writeI32(int32(v.ContactType))"), - "enum fields pack as i32: {go}" - ); - assert!( - go.contains("v.ContactType = ContactType(r.readI32())"), - "enum fields decode through the enum type: {go}" - ); -} - -#[test] -fn rich_enum_is_sealed_sum_type() { - let go = rg(&shapes_api()); - assert!( - go.contains("type Shape interface {"), - "missing sealed interface: {go}" - ); - assert!(go.contains("\tisShape()\n"), "missing sealing method: {go}"); - assert!( - go.contains("type ShapeEmpty struct{}"), - "unit variant should be an empty struct: {go}" - ); - assert!( - go.contains("type ShapeCircle struct {") && go.contains("\tRadius float64\n"), - "data variant carries typed fields: {go}" - ); - assert!( - go.contains("type ShapeLabeled struct {") - && go.contains("\tLabel string\n") - && go.contains("\tCount uint8\n"), - "multi-field variant carries all fields: {go}" - ); - assert!( - go.contains("func (ShapeEmpty) isShape() {}") - && go.contains("func (ShapeCircle) isShape() {}") - && go.contains("func (ShapeLabeled) isShape() {}"), - "every variant implements the sealing method: {go}" - ); - // Rich enums have no C symbols. - assert!( - !go.contains("Shape_destroy") && !go.contains("NewShapeCircle") && !go.contains("Tag()"), - "rich enums have no constructors, tag readers, or destroy: {go}" - ); -} - -#[test] -fn rich_enum_pack_unpack() { - let go = rg(&shapes_api()); - assert!( - go.contains("func wvPackShape(w *wvWriter, v Shape) {"), - "missing pack function: {go}" - ); - assert!( - go.contains("switch x := v.(type) {"), - "pack switches on the variant type: {go}" - ); - assert!( - go.contains("case ShapeCircle:") - && go.contains("w.writeI32(1)") - && go.contains("w.writeF64(x.Radius)"), - "pack writes the tag then the variant fields: {go}" - ); - assert!( - go.contains("case ShapeLabeled:") - && go.contains("w.writeI32(3)") - && go.contains("w.writeString(x.Label)") - && go.contains("w.writeU8(x.Count)"), - "non-contiguous tags use the declared values: {go}" - ); - assert!( - go.contains("panic(\"weaveffi: Shape value is not one of its variants\")"), - "pack rejects foreign implementations: {go}" - ); - assert!( - go.contains("func wvUnpackShape(r *wvReader) Shape {"), - "missing unpack function: {go}" - ); - assert!( - go.contains("return ShapeEmpty{}"), - "unit variants decode to the empty struct: {go}" - ); - assert!( - go.contains("x.Radius = r.readF64()"), - "variant fields decode in order: {go}" - ); - assert!( - go.contains("panic(\"weaveffi: malformed value buffer: Shape tag out of range\")"), - "unpack rejects unknown tags: {go}" - ); - // The rich enum crosses the ABI as a buffer in both directions. - assert!( - go.contains("func Describe(shape Shape) string {"), - "rich enum param is the bare interface type: {go}" - ); - assert!( - go.contains("wvPackShape(wShape, shape)"), - "rich enum param packs through the writer: {go}" - ); - assert!( - go.contains("func Scale(shape Shape, factor float64) Shape {"), - "rich enum return is the bare interface type: {go}" - ); - assert!( - go.contains("goResult = wvUnpackShape(rRes)"), - "rich enum return decodes from the buffer: {go}" - ); -} - -#[test] -fn no_bool_helpers_when_unneeded() { - let mut m = module("math"); - m.functions = vec![func_of( - "add", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - )]; - let go = rg(&api_of(vec![m])); - assert!( - !go.contains("boolToC"), - "should not include bool helpers: {go}" - ); -} - -// ── The value-buffer runtime ── - -#[test] -fn buffer_runtime_emitted_once() { - let mut a = module("alpha"); - a.structs = vec![StructDef { - name: "A".into(), - doc: None, - fields: vec![field("x", TypeRef::I32)], - }]; - let mut b = module("beta"); - b.structs = vec![StructDef { - name: "B".into(), - doc: None, - fields: vec![field("y", TypeRef::F32)], - }]; - let go = rg(&api_of(vec![a, b])); - assert_eq!( - go.matches("type wvWriter struct {").count(), - 1, - "runtime must be emitted exactly once: {go}" - ); - assert_eq!( - go.matches("type wvReader struct {").count(), - 1, - "runtime must be emitted exactly once: {go}" - ); - assert!( - go.contains("binary.LittleEndian"), - "wire format is little-endian: {go}" - ); - assert!( - go.contains("if !utf8.Valid(b) {"), - "string decode must validate UTF-8: {go}" - ); - assert!( - go.contains("wvMalformed(\"length prefix exceeds remaining buffer\")"), - "reader must reject oversized length prefixes: {go}" - ); - assert!( - go.contains("wvMalformed(\"trailing bytes after value\")"), - "reader must reject trailing bytes: {go}" - ); - assert!( - go.contains("C.weaveffi_free_bytes(ptr, length)"), - "wvCopyBuffer must free the producer buffer: {go}" - ); - assert!( - go.contains("\t\"encoding/binary\"\n") - && go.contains("\t\"math\"\n") - && go.contains("\t\"unicode/utf8\"\n"), - "runtime imports must be present: {go}" - ); -} - -#[test] -fn no_buffer_runtime_when_unneeded() { - let go = rg(&calculator_api()); - assert!( - !go.contains("wvWriter"), - "scalar-only surfaces need no buffer runtime: {go}" - ); - assert!( - !go.contains("\"encoding/binary\""), - "scalar-only surfaces must not import binary: {go}" - ); -} - -// ── Typed handles ── - -#[test] -fn typed_handle_wrapper_and_flow() { - let mut m = module("vault"); - m.structs = vec![StructDef { - name: "Session".into(), - doc: None, - fields: vec![field("token", TypeRef::TypedHandle("Token".into()))], - }]; - m.functions = vec![ - func_of("open", vec![], Some(TypeRef::TypedHandle("Token".into()))), - func_of( - "revoke", - vec![param("t", TypeRef::TypedHandle("Token".into()))], - None, - ), - ]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("type TokenHandle struct {"), - "missing handle wrapper: {go}" - ); - assert!( - go.contains("ptr *C.weaveffi_vault_Token"), - "wrapper must hold the opaque C pointer: {go}" - ); - assert!( - go.contains("func Open() *TokenHandle {"), - "handle return should be the wrapper pointer: {go}" - ); - assert!( - go.contains("return &TokenHandle{ptr: result}"), - "missing handle wrap on return: {go}" - ); - assert!( - go.contains("C.weaveffi_vault_revoke(t.ptr, &cErr)"), - "handle params pass the wrapped pointer: {go}" - ); - // No destroy: a typed handle is a borrowed id. - assert!( - !go.contains("func (s *TokenHandle) Close()"), - "typed handles owe no release call: {go}" - ); - // Inside buffers the handle serializes as the pointer's u64 value. - assert!( - go.contains("w.writeU64(uint64(uintptr(unsafe.Pointer(v.Token.ptr))))"), - "handle fields pack as u64: {go}" - ); - assert!( - go.contains( - "v.Token = &TokenHandle{ptr: (*C.weaveffi_vault_Token)(unsafe.Pointer(uintptr(r.readU64())))}" - ), - "handle fields decode back into the wrapper: {go}" - ); -} - -// ── Async ── - -/// Async functions get a blocking wrapper: a registry-id context, an -/// exported completion trampoline, and a buffered channel the wrapper -/// waits on. The channel is buffered so the producer thread never blocks -/// on the send even if the waiter has already given up. -#[test] -fn go_async_generates_blocking_wrapper() { - let mut m = module("io"); - m.functions = vec![ - { - let mut f = func_of("read", vec![], Some(TypeRef::StringUtf8)); - f.r#async = true; - f - }, - func_of("write", vec![], None), - ]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("//export goWv_weaveffi_io_read_callback"), - "completion trampoline must be exported: {go}" - ); - assert!( - go.contains("extern void goWv_weaveffi_io_read_callback(void* context, weaveffi_error* err, char* result);"), - "preamble must declare the trampoline const-free: {go}" - ); - assert!( - go.contains("C.weaveffi_io_read_async("), - "async launcher must be invoked: {go}" - ); - assert!( - go.contains("func Read() string {"), - "plain async wrapper must have a bare return: {go}" - ); - assert!( - go.contains("ch := make(chan wvOutcomeIoRead, 1)"), - "wrapper must wait on a buffered outcome channel: {go}" - ); - assert!( - go.contains("panic(outcome.err)"), - "plain async wrapper must panic on a reported error: {go}" - ); - assert!( - go.contains("ch <- wvOutcomeIoRead{err: wvBrandError(wvTakeBoxedError(err))}"), - "plain async trampoline brands the error, never the domain: {go}" - ); - assert!( - go.contains("return outcome.val"), - "plain async wrapper returns the outcome value: {go}" - ); - // The completion callback owns its result buffers: copy, then free. - assert!( - go.contains("C.weaveffi_free_string(result)"), - "owned async result buffers must be freed after copying: {go}" - ); - assert!( - go.contains("val = C.GoString(result)"), - "async string results must be copied before the callback returns: {go}" - ); - assert!( - go.contains("// Blocks the calling goroutine until the async producer completes."), - "async wrapper must document that it blocks: {go}" - ); - assert!( - go.contains("weaveffi_io_write"), - "sync function should still be emitted: {go}" - ); - assert!(go.contains("\t\"sync\"\n"), "sync import needed: {go}"); -} - -#[test] -fn async_cancellable_passes_null_token() { - let mut m = module("tasks"); - m.functions = vec![{ - let mut f = func_of("run", vec![], Some(TypeRef::I32)); - f.r#async = true; - f.cancellable = true; - f - }]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("func Run() int32 {"), - "async wrapper must be generated: {go}" - ); - assert!( - go.contains("C.weaveffi_tasks_run_async(nil, "), - "cancel token must be passed as NULL: {go}" - ); -} - -#[test] -fn async_record_result_decodes_owned_buffer() { - let mut m = module("metrics"); - m.structs = vec![StructDef { - name: "Stats".into(), - doc: None, - fields: vec![field("total", TypeRef::I64)], - }]; - m.functions = vec![{ - let mut f = func_of("load", vec![], Some(TypeRef::Record("Stats".into()))); - f.r#async = true; - f - }]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains( - "extern void goWv_weaveffi_metrics_load_callback(void* context, weaveffi_error* err, uint8_t* result_ptr, size_t result_len);" - ), - "buffered async callback carries borrowed ptr + len slots: {go}" - ); - assert!( - go.contains("rRes := &wvReader{buf: wvCopyBuffer(result_ptr, result_len)}"), - "async result buffer is owned: copied, then freed: {go}" - ); - assert!( - go.contains("val = wvUnpackStats(rRes)"), - "async record result decodes inside the trampoline: {go}" - ); - assert!( - go.contains("func Load() Stats {"), - "async record wrapper returns the value struct: {go}" - ); -} - -// ── Listeners and callbacks ── - -#[test] -fn listeners_generate_register_unregister() { - let mut m = module("events"); - m.callbacks = vec![CallbackDef { - name: "OnMessage".into(), - doc: None, - params: vec![param("message", TypeRef::StringUtf8)], - }]; - m.listeners = vec![ListenerDef { - name: "message_listener".into(), - event_callback: "OnMessage".into(), - doc: None, - }]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("//export goWv_weaveffi_events_OnMessage_fn"), - "callback trampoline must be exported: {go}" - ); - assert!( - go.contains("extern void goWv_weaveffi_events_OnMessage_fn(char* message, void* context);"), - "preamble must declare the trampoline: {go}" - ); - assert!( - go.contains("func RegisterMessageListener(callback func(message string)) uint64 {"), - "register wrapper must be emitted with the stripped name: {go}" - ); - assert!( - go.contains("func UnregisterMessageListener(id uint64) {"), - "unregister wrapper must be emitted with the stripped name: {go}" - ); - assert!( - go.contains("C.weaveffi_events_register_message_listener(C.weaveffi_events_OnMessage_fn(unsafe.Pointer(C.goWv_weaveffi_events_OnMessage_fn)), unsafe.Pointer(uintptr(ctxID)))"), - "register must pass the shared trampoline and registry id: {go}" - ); - assert!( - go.contains("wvListenerCtx[id] = ctxID"), - "subscription must retain the Go callback: {go}" - ); -} - -#[test] -fn callback_buffered_param_decodes_borrowed_buffer() { - let mut m = module("feed"); - m.callbacks = vec![CallbackDef { - name: "OnBatch".into(), - doc: None, - params: vec![param("items", TypeRef::List(Box::new(TypeRef::StringUtf8)))], - }]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains( - "extern void goWv_weaveffi_feed_OnBatch_fn(uint8_t* items_ptr, size_t items_len, void* context);" - ), - "buffered callback param carries ptr + len slots: {go}" - ); - assert!( - go.contains("rArg0 := &wvReader{buf: wvBorrowBuffer(items_ptr, items_len)}"), - "callback buffers are borrowed, never freed: {go}" - ); - assert!( - go.contains("arg0 = make([]string, nArg00)"), - "list argument decodes before dispatch: {go}" - ); - assert!( - go.contains("cb(arg0)"), - "decoded value is handed to the user callback: {go}" - ); -} - -// ── Interfaces ── - -#[test] -fn interface_wrapper_and_ctor() { - let go = rg(&kv_api()); - assert!( - go.contains("type Store struct {"), - "missing interface wrapper struct: {go}" - ); - assert!( - go.contains("ptr *C.weaveffi_kv_Store"), - "missing wrapped C pointer: {go}" - ); - // Factory constructor: `open` -> `OpenStore`, throwing. - assert!( - go.contains("func OpenStore(path string) (*Store, error) {"), - "missing factory constructor: {go}" - ); - assert!( - go.contains("result := C.weaveffi_kv_Store_open(cPath, &cErr)"), - "ctor must call the member symbol: {go}" - ); - assert!( - go.contains("return nil, wvMapKv(wvTakeError(&cErr))"), - "throwing ctor maps the domain error: {go}" - ); - assert!( - go.contains("return &Store{ptr: result}, nil"), - "ctor wraps the owned pointer: {go}" - ); -} - -#[test] -fn interface_new_ctor_naming() { - let go = rg(&contacts_api()); - assert!( - go.contains("func NewContactBook() *ContactBook {"), - "ctor named `new` must surface as New: {go}" - ); - assert!( - go.contains("result := C.weaveffi_contacts_ContactBook_new(&cErr)"), - "missing ctor symbol call: {go}" - ); - assert!( - go.contains("return &ContactBook{ptr: result}"), - "plain ctor wraps without error: {go}" - ); -} - -#[test] -fn interface_methods_pass_self() { - let go = rg(&kv_api()); - // Throwing method: `(T, error)` with the receiver's ptr leading. The - // optional scalar parameter is buffered now. - assert!( - go.contains( - "func (s *Store) Put(key string, value []byte, kind EntryKind, ttlSeconds *int64) (bool, error) {" - ), - "missing throwing method: {go}" - ); - assert!( - go.contains("result := C.weaveffi_kv_Store_put(s.ptr, cKey, cValuePtr, cValueLen, C.weaveffi_kv_EntryKind(kind), cTtlSecondsPtr, C.size_t(len(wTtlSeconds.buf)), &cErr)"), - "method must pass s.ptr and the buffered optional's ptr + len: {go}" - ); - assert!( - go.contains("wTtlSeconds.writeI64((*ttlSeconds))"), - "optional scalar param packs into the writer: {go}" - ); - assert!( - go.contains("return false, wvMapKv(wvTakeError(&cErr))"), - "throwing bool method returns its zero value with the error: {go}" - ); - // Optional record return through a method decodes from the buffer. - assert!( - go.contains("func (s *Store) Get(key string) (*Entry, error) {"), - "missing optional-return method: {go}" - ); - assert!( - go.contains("oRes0 = wvUnpackEntry(rRes)"), - "optional record return decodes the present value: {go}" - ); - // Plain method: bare return, traps. - assert!( - go.contains("func (s *Store) Count() int64 {"), - "missing plain method: {go}" - ); - assert!( - go.contains("result := C.weaveffi_kv_Store_count(s.ptr, &cErr)"), - "plain method must pass s.ptr: {go}" - ); - // Plain void method. - assert!( - go.contains("func (s *Store) Clear() {"), - "missing plain void method: {go}" - ); - // Deprecated member keeps its notice. - assert!( - go.contains("// Deprecated: use put() with explicit kind"), - "missing deprecation notice: {go}" - ); -} - -#[test] -fn interface_static_naming() { - let go = rg(&kv_api()); - assert!( - go.contains("func StoreDefaultCapacity() int64 {"), - "statics are package-level, namespaced by the type: {go}" - ); - assert!( - go.contains("C.weaveffi_kv_Store_default_capacity(&cErr)"), - "static must call the member symbol without self: {go}" - ); -} - -#[test] -fn interface_close_calls_destroy() { - let go = rg(&kv_api()); - assert!( - go.contains("func (s *Store) Close() {"), - "missing Close: {go}" - ); - assert!( - go.contains("C.weaveffi_kv_Store_destroy(s.ptr)"), - "Close must call the destroy symbol: {go}" - ); -} - -#[test] -fn optional_interface_param_stays_pointer() { - let mut m = module("kv"); - m.interfaces = vec![InterfaceDef { - name: "Store".into(), - doc: None, - constructors: vec![], - methods: vec![], - statics: vec![], - }]; - m.functions = vec![func_of( - "inspect", - vec![param( - "store", - TypeRef::Optional(Box::new(TypeRef::Interface("Store".into()))), - )], - None, - )]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("func Inspect(store *Store) {"), - "optional interface param stays a nullable wrapper pointer: {go}" - ); - assert!( - go.contains("var cStore *C.weaveffi_kv_Store"), - "missing nullable C pointer staging: {go}" - ); - assert!( - go.contains("cStore = store.ptr"), - "present value passes the wrapped pointer: {go}" - ); - assert!( - !go.contains("wStore"), - "optional interfaces are never buffered: {go}" - ); -} - -#[test] -fn interface_async_method_throws() { - let go = rg(&kv_api()); - assert!( - go.contains("func (s *Store) Compact() (int64, error) {"), - "async throwing method keeps (T, error): {go}" - ); - assert!( - go.contains("type wvOutcomeKvStoreCompact struct {"), - "outcome type derives from the member symbol: {go}" - ); - assert!( - go.contains("//export goWv_weaveffi_kv_Store_compact_callback"), - "member trampoline must be exported: {go}" - ); - assert!( - go.contains("C.weaveffi_kv_Store_compact_async(s.ptr, nil, "), - "launch passes s.ptr then the NULL cancel token: {go}" - ); - assert!( - go.contains("ch <- wvOutcomeKvStoreCompact{err: wvMapKv(wvTakeBoxedError(err))}"), - "trampoline maps the domain error: {go}" - ); - assert!( - go.contains("return 0, outcome.err"), - "throwing async wrapper returns the outcome error: {go}" - ); -} - -#[test] -fn interface_iterator_method_throws() { - let go = rg(&kv_api()); - // A throwing iterator returns iter.Seq2[T, error]; the standard iter - // package is imported. - assert!( - go.contains("func (s *Store) ListKeys(prefix *string) iter.Seq2[string, error] {"), - "throwing iterator method returns iter.Seq2[T, error]: {go}" - ); - assert!(go.contains("\t\"iter\"\n"), "iter import needed: {go}"); - // The launch runs lazily inside the returned closure (first pull), - // never in the wrapper body itself. The optional string param is - // buffered and staged inside the closure. - let fn_start = go - .find("func (s *Store) ListKeys(") - .expect("ListKeys wrapper"); - let fn_text = &go[fn_start..]; - let closure = fn_text - .find("return func(yield func(string, error) bool) {") - .expect("sequence closure in ListKeys"); - let launch = fn_text - .find( - "it := C.weaveffi_kv_Store_list_keys(s.ptr, cPrefixPtr, C.size_t(len(wPrefix.buf)), &cErr)", - ) - .expect("launch in ListKeys"); - assert!( - closure < launch, - "launch must run inside the closure: {fn_text}" - ); - // Launch errors are yielded as the final (zero, err) pair. - assert!( - go.contains("yield(\"\", wvMapKv(wvTakeError(&cErr)))"), - "launch errors are yielded through the domain: {go}" - ); - // Destroy is deferred inside the closure so an early break still - // destroys exactly once. - assert!( - go.contains("defer C.weaveffi_kv_Store_ListKeysIterator_destroy(it)"), - "iterator destroy must be deferred inside the closure: {go}" - ); - // One producer next call per consumer step. - assert!( - go.contains("ok := C.weaveffi_kv_Store_ListKeysIterator_next(it, &outItem, &iterErr) != 0"), - "iterator must pull one element per step: {go}" - ); - assert!( - go.contains("yield(\"\", wvMapKv(wvTakeError(&iterErr)))"), - "per-element errors are yielded through the domain: {go}" - ); - // Each yielded string element is freed after copying. - assert!( - go.contains("item := C.GoString(outItem)\n\t\t\tC.weaveffi_free_string(outItem)"), - "string elements must be freed after copying: {go}" - ); - assert!( - go.contains("if !yield(item, nil) {"), - "elements are yielded with a nil error: {go}" - ); - // No hidden drain into a slice. - assert!( - !fn_text[..fn_text.find("\n}\n").unwrap()].contains("append("), - "iterator must not drain into a slice: {fn_text}" - ); -} - -#[test] -fn plain_iterator_function_traps() { - let mut m = module("events"); - m.functions = vec![func_of( - "get_messages", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - )]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("func GetMessages() iter.Seq[string] {"), - "plain iterator returns iter.Seq[T]: {go}" - ); - assert!( - go.contains("return func(yield func(string) bool) {"), - "plain iterator returns a single-value sequence closure: {go}" - ); - assert!( - go.contains("wvTrap(&cErr)"), - "plain iterator traps launch errors: {go}" - ); - assert!( - go.contains("wvTrap(&iterErr)"), - "plain iterator traps per-element errors: {go}" - ); - assert!( - go.contains("defer C.weaveffi_events_GetMessagesIterator_destroy(it)"), - "plain iterator defers destroy inside the closure: {go}" - ); - assert!( - go.contains("if !yield(item) {"), - "plain iterator yields bare elements: {go}" - ); - assert!( - !go.contains("func GetMessages() []string"), - "plain iterator must not drain into a slice: {go}" - ); -} - -#[test] -fn iterator_buffered_elements_decode_and_free() { - let mut m = module("contacts"); - m.structs = vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }]; - m.functions = vec![func_of( - "iter_contacts", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::Record( - "Contact".into(), - )))), - )]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("func IterContacts() iter.Seq[Contact] {"), - "record iterator yields value structs: {go}" - ); - assert!( - go.contains("var outItem *C.uint8_t") && go.contains("var outLen C.size_t"), - "buffered elements arrive as ptr + len slots: {go}" - ); - assert!( - go.contains("(it, &outItem, &outLen, &iterErr) != 0"), - "next must pass the element length slot: {go}" - ); - assert!( - go.contains("rItem := &wvReader{buf: wvCopyBuffer(outItem, outLen)}"), - "each element must be copied and freed through wvCopyBuffer: {go}" - ); - assert!( - go.contains("item = wvUnpackContact(rItem)"), - "each element decodes through the record unpack: {go}" - ); - assert!( - go.contains("rItem.expectEnd()"), - "element decode must reject trailing bytes: {go}" - ); -} - -#[test] -fn cross_module_interface_param_borrows() { - let go = rg(&kv_api()); - assert!( - go.contains("func GetStats(store *Store) (Stats, error) {"), - "nested-module function takes the wrapper, returns the record value: {go}" - ); - assert!( - go.contains("result := C.weaveffi_kv_stats_get_stats(store.ptr, &cOutLen, &cErr)"), - "interface params borrow the wrapped pointer: {go}" - ); - assert!( - go.contains("return Stats{}, wvMapKv(wvTakeError(&cErr))"), - "inheriting submodule maps through the ancestor domain, zeroing the record: {go}" - ); - assert!( - go.contains("goResult = wvUnpackStats(rRes)"), - "cross-module record return decodes from the buffer: {go}" - ); -} - -#[test] -fn typed_error_emitted_once_with_all_codes() { - let go = rg(&kv_api()); - assert_eq!( - go.matches("type KvError struct {").count(), - 1, - "domain type must be emitted exactly once: {go}" - ); - assert!(go.contains("KvErrorKeyNotFound int32 = 1001"), "{go}"); - assert!(go.contains("KvErrorExpired int32 = 1002"), "{go}"); - assert!(go.contains("KvErrorStoreFull int32 = 1003"), "{go}"); - assert!(go.contains("KvErrorIoError int32 = 1004"), "{go}"); - assert!( - go.contains("func wvMapKv(code int32, message string, payload []byte) error {"), - "missing wvMapKv helper: {go}" - ); - assert!( - go.contains("case KvErrorKeyNotFound:"), - "mapping must switch on the code constants: {go}" - ); -} - -#[test] -fn kv_listener_uses_stripped_names() { - let go = rg(&kv_api()); - assert!( - go.contains("func RegisterEvictionListener(callback func(key string)) uint64 {"), - "{go}" - ); - assert!( - go.contains("func UnregisterEvictionListener(id uint64) {"), - "{go}" - ); -} - -// ── Naming ── - -#[test] -fn module_prefix_stripping_default_and_knob() { - let api = calculator_api(); - let stripped = rg(&api); - assert!( - stripped.contains("func Add(a int32, b int32) int32 {"), - "stripping is the default: {stripped}" - ); - assert!( - !stripped.contains("func CalculatorAdd("), - "stripped output must not keep the module prefix: {stripped}" - ); - let prefixed = rg_with(&api, "weaveffi", false); - assert!( - prefixed.contains("func CalculatorAdd(a int32, b int32) int32 {"), - "knob off restores the module prefix: {prefixed}" - ); -} - -#[test] -fn nested_module_stripping() { - let go = rg_with(&kv_api(), "weaveffi", false); - assert!( - go.contains("func KvStatsGetStats(store *Store)"), - "unstripped nested-module functions carry the full path: {go}" - ); - // Interface members are namespaced by their type, never the module. - assert!( - go.contains("func (s *Store) Put("), - "interface members are unaffected by the knob: {go}" - ); - assert!( - go.contains("func OpenStore(path string)"), - "constructors are unaffected by the knob: {go}" - ); -} - -#[test] -fn contacts_surface_matches_cli_expectations() { - let go = rg(&contacts_api()); - assert!(go.contains("type ContactType int32"), "{go}"); - assert!(go.contains("type Contact struct {"), "{go}"); - assert!(go.contains("\tFirstName string\n"), "{go}"); - assert!(go.contains("\tEmail *string\n"), "{go}"); - assert!(go.contains("type ContactBook struct {"), "{go}"); - assert!(go.contains("ptr *C.weaveffi_contacts_ContactBook"), "{go}"); - assert!( - go.contains("func (s *ContactBook) Add(firstName string, lastName string, email *string, contactType ContactType) (Contact, error) {"), - "{go}" - ); - assert!( - go.contains("func (s *ContactBook) Get(id int64) (Contact, error) {"), - "{go}" - ); - assert!( - go.contains("func (s *ContactBook) List() []Contact {"), - "{go}" - ); - assert!( - go.contains("func (s *ContactBook) Remove(id int64) bool {"), - "{go}" - ); - assert!(go.contains("func (s *ContactBook) Count() int32 {"), "{go}"); - assert!(go.contains("func (s *ContactBook) Close() {"), "{go}"); - assert!( - go.contains("C.weaveffi_contacts_ContactBook_destroy(s.ptr)"), - "{go}" - ); - assert!(go.contains("type ContactsError struct {"), "{go}"); - assert!(go.contains("ContactsErrorInvalidName int32 = 1"), "{go}"); - assert!(go.contains("ContactsErrorNotFound int32 = 2"), "{go}"); - assert!( - go.contains("func wvMapContacts(code int32, message string, payload []byte) error {"), - "{go}" - ); - assert!( - go.contains("return Contact{}, wvMapContacts(wvTakeError(&cErr))"), - "{go}" - ); -} - -// ── Generate-to-disk paths ── - -#[test] -fn generates_file_on_disk() { - let api = calculator_api(); - let tmp = std::env::temp_dir().join("weaveffi_test_go_gen"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8"); - - GoGenerator - .generate(&api, out_dir, &GoConfig::default()) - .unwrap(); - - let go_file = tmp.join("go/weaveffi.go"); - assert!(go_file.exists(), "go/weaveffi.go should exist"); - let contents = std::fs::read_to_string(&go_file).unwrap(); - assert!( - contents.contains("package weaveffi"), - "file should contain package declaration" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn go_generates_go_mod() { - let api = calculator_api(); - let tmp = std::env::temp_dir().join("weaveffi_test_go_mod"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - GoGenerator - .generate(&api, out_dir, &GoConfig::default()) - .unwrap(); - - let go_mod_path = tmp.join("go/go.mod"); - assert!(go_mod_path.exists(), "go/go.mod should exist"); - let go_mod = std::fs::read_to_string(&go_mod_path).unwrap(); - assert!( - go_mod.contains("module weaveffi"), - "missing module directive: {go_mod}" - ); - assert!( - go_mod.contains("go 1.23"), - "go.mod must require Go 1.23 for the iter package: {go_mod}" - ); - - let readme_path = tmp.join("go/README.md"); - assert!(readme_path.exists(), "go/README.md should exist"); - let readme = std::fs::read_to_string(&readme_path).unwrap(); - assert!( - readme.contains("CGo"), - "README should mention CGo: {readme}" - ); - assert!( - readme.contains("go build"), - "README should mention go build: {readme}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn generate_go_basic() { - let api = calculator_api(); - let tmp = std::env::temp_dir().join("weaveffi_test_go_basic"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - GoGenerator - .generate(&api, out_dir, &GoConfig::default()) - .unwrap(); - - let go = std::fs::read_to_string(tmp.join("go/weaveffi.go")).unwrap(); - assert!(go.contains("package weaveffi"), "missing package: {go}"); - assert!( - go.contains("func Add(a int32, b int32) int32 {"), - "missing add function: {go}" - ); - assert!( - go.contains("func Echo(msg string) string {"), - "missing echo function: {go}" - ); - - let go_mod = std::fs::read_to_string(tmp.join("go/go.mod")).unwrap(); - assert!( - go_mod.contains("module weaveffi"), - "go.mod should have default module path: {go_mod}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn go_custom_module_path() { - let api = calculator_api(); - let tmp = std::env::temp_dir().join("weaveffi_test_go_custom_mod"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - let config = GoConfig { - module_path: Some("github.com/myorg/mylib".into()), - ..GoConfig::default() - }; - GoGenerator.generate(&api, out_dir, &config).unwrap(); - - let go_mod = std::fs::read_to_string(tmp.join("go/go.mod")).unwrap(); - assert!( - go_mod.contains("module github.com/myorg/mylib"), - "go.mod should use custom module path: {go_mod}" - ); - assert!( - !go_mod.contains("module weaveffi"), - "go.mod should not use default path: {go_mod}" - ); - - let go = std::fs::read_to_string(tmp.join("go/weaveffi.go")).unwrap(); - assert!( - go.contains("package weaveffi"), - "Go source should still use weaveffi package: {go}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -// ── Ordering and memory-safety details ── - -#[test] -fn go_no_double_free_on_error() { - let mut m = module("contacts"); - m.structs = vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }]; - m.functions = vec![func_of( - "find_contact", - vec![param("name", TypeRef::StringUtf8)], - Some(TypeRef::Record("Contact".into())), - )]; - let go = rg(&api_of(vec![m])); - - let fn_start = go.find("func FindContact(").expect("FindContact wrapper"); - let fn_body = &go[fn_start..]; - let fn_end = fn_body.find("\n}\n").unwrap(); - let fn_text = &fn_body[..fn_end]; - - assert!( - !fn_text.contains("weaveffi_free_string(cName"), - "borrowed string param must not be freed via weaveffi_free_string: {fn_text}" - ); - - let err_check = fn_text - .find("wvTrap(&cErr)") - .expect("trap check in FindContact"); - let decode = fn_text - .find("wvCopyBuffer(result, cOutLen)") - .expect("buffered decode in FindContact"); - assert!( - err_check < decode, - "error must be checked before decoding the return buffer: {fn_text}" - ); -} - -#[test] -fn go_flag_check_on_optional_return() { - let mut m = module("contacts"); - m.structs = vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }]; - m.functions = vec![func_of( - "find_contact", - vec![param("id", TypeRef::I32)], - Some(TypeRef::Optional(Box::new(TypeRef::Record( - "Contact".into(), - )))), - )]; - let go = rg(&api_of(vec![m])); - - let fn_start = go.find("func FindContact(").expect("FindContact wrapper"); - let fn_body = &go[fn_start..]; - let fn_end = fn_body.find("\n}\n").unwrap(); - let fn_text = &fn_body[..fn_end]; - - let flag_check = fn_text - .find("if rRes.readOptionFlag() {") - .expect("flag check in FindContact"); - let decode = fn_text - .find("wvUnpackContact(rRes)") - .expect("Contact decode in FindContact"); - assert!( - flag_check < decode, - "optional record return must check the flag before decoding: {fn_text}" - ); -} - -#[test] -fn string_list_return_decodes_from_buffer() { - let mut m = module("store"); - m.functions = vec![func_of( - "list_keys", - vec![], - Some(TypeRef::List(Box::new(TypeRef::StringUtf8))), - )]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("rRes := &wvReader{buf: wvCopyBuffer(result, cOutLen)}"), - "list return decodes from one owned buffer: {go}" - ); - assert!( - go.contains("goResult[iRes0] = rRes.readString()"), - "string elements decode in place: {go}" - ); - assert!( - !go.contains("unsafe.Slice("), - "parallel-array decoding is gone: {go}" - ); -} - -// ── Docs ── - -fn doc_api() -> ResolvedApi { - let mut m = module("docs"); - m.functions = vec![Function { - name: "do_thing".into(), - params: vec![Param { - name: "x".into(), - ty: TypeRef::I32, - mutable: false, - doc: Some("the input value".into()), - }], - returns: Some(TypeRef::I32), - doc: Some("Performs a thing.".into()), - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }]; - m.structs = vec![StructDef { - name: "Item".into(), - doc: Some("An item we track.".into()), - fields: vec![StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: Some("Stable id".into()), - }], - }]; - m.enums = vec![EnumDef { - name: "Kind".into(), - doc: Some("Kind of item.".into()), - variants: vec![EnumVariant { - name: "Small".into(), - value: 0, - doc: Some("A small one".into()), - fields: vec![], - }], - }]; - api_of(vec![m]) -} - -#[test] -fn go_emits_doc_on_function() { - let go = rg(&doc_api()); - assert!(go.contains("// DoThing: Performs a thing."), "{go}"); -} - -#[test] -fn go_emits_doc_on_struct() { - let go = rg(&doc_api()); - assert!(go.contains("// Item: An item we track."), "{go}"); -} - -#[test] -fn go_emits_doc_on_enum_variant() { - let go = rg(&doc_api()); - assert!(go.contains("// Kind: Kind of item."), "{go}"); - assert!(go.contains("// KindSmall: A small one"), "{go}"); -} - -#[test] -fn go_emits_doc_on_field() { - let go = rg(&doc_api()); - assert!(go.contains("// Id: Stable id"), "{go}"); -} - -#[test] -fn go_emits_doc_on_param() { - let go = rg(&doc_api()); - assert!(go.contains("// Parameters:"), "{go}"); - assert!(go.contains("// - x: the input value"), "{go}"); -} - -#[test] -fn go_custom_prefix_threads_to_user_symbols() { - let go = rg_with(&calculator_api(), "myffi", true); - // User symbols adopt the configured prefix. - assert!( - go.contains("C.myffi_calculator_add("), - "user symbol should use the custom prefix: {go}" - ); - assert!( - !go.contains("weaveffi_calculator_add"), - "user symbol must not keep the default prefix: {go}" - ); - // The cgo preamble includes the prefixed C header. - assert!( - go.contains("#include \"myffi.h\""), - "cgo preamble should include the prefixed header: {go}" - ); - // Runtime ABI helpers exported by weaveffi-abi stay literal. - assert!( - go.contains("C.weaveffi_free_string(result)"), - "runtime helper weaveffi_free_string must stay literal: {go}" - ); - assert!( - go.contains("var cErr C.weaveffi_error"), - "runtime helper weaveffi_error must stay literal: {go}" - ); -} - -#[test] -fn reserved_go_keywords_escape_in_param_positions() { - let mut m = module("meta"); - m.functions = vec![func_of( - "frob", - vec![ - param("type", TypeRef::I32), - param("func", TypeRef::StringUtf8), - ], - Some(TypeRef::I32), - )]; - m.callbacks = vec![CallbackDef { - name: "OnRange".into(), - doc: None, - params: vec![param("range", TypeRef::StringUtf8)], - }]; - let go = rg(&api_of(vec![m])); - assert!( - go.contains("func Frob(type_ int32, func_ string) int32 {"), - "keyword params must gain a trailing underscore: {go}" - ); - assert!( - go.contains("C.int32_t(type_)"), - "escaped param must thread into the scalar conversion: {go}" - ); - assert!( - go.contains("cFunc := C.CString(func_)"), - "staging locals derive from the escaped spelling: {go}" - ); - assert!( - go.contains("cb := v.(func(range_ string))"), - "callback signatures escape keyword params too: {go}" - ); -} - -#[test] -fn negative_codes_fall_back_to_the_brand_error() { - // The ABI reserves all negative codes for the runtime (-1 generic, -2 - // producer panic, -3 marshalling failure); domain codes are validated - // positive-only. The mapping helper must therefore route any negative - // code through its default arm to the generic brand error, never a typed - // domain case. - let go = rg(&store_api()); - let map_start = go.find("func wvMapStore(").expect("mapping helper"); - let map_text = &go[map_start..]; - let map_text = &map_text[..map_text.find("\n}\n").unwrap()]; - assert!( - !map_text.contains("case -"), - "no negative code may match a domain case: {map_text}" - ); - assert!( - map_text.contains("default:") - && map_text.contains("return wvBrandError(code, message, payload)"), - "unmatched (including negative) codes must brand generically: {map_text}" - ); -} diff --git a/crates/weaveffi-gen-go/src/types.rs b/crates/weaveffi-gen-go/src/types.rs index 4dbe2acd..1c13f5bb 100644 --- a/crates/weaveffi-gen-go/src/types.rs +++ b/crates/weaveffi-gen-go/src/types.rs @@ -5,8 +5,8 @@ use heck::{ToLowerCamelCase, ToUpperCamelCase}; use weaveffi_core::abi::{CType, ConstPos}; use weaveffi_core::lang; +use weaveffi_core::model::Ty; use weaveffi_core::utils::{c_abi_struct_name, local_type_name}; -use weaveffi_ir::ir::TypeRef; /// The local Go type name (PascalCase) of a user-defined type reference, /// stripping any qualifying module path. @@ -31,30 +31,30 @@ pub(crate) fn go_param_ident(name: &str) -> String { } /// The Go type spelling of an IR type reference. -pub(crate) fn go_type(ty: &TypeRef) -> String { +pub(crate) fn go_type(ty: &Ty) -> String { match ty { - TypeRef::I8 => "int8".into(), - TypeRef::I16 => "int16".into(), - TypeRef::I32 => "int32".into(), - TypeRef::U8 => "uint8".into(), - TypeRef::U16 => "uint16".into(), - TypeRef::U32 => "uint32".into(), - TypeRef::U64 => "uint64".into(), - TypeRef::I64 | TypeRef::Handle => "int64".into(), - TypeRef::F32 => "float32".into(), - TypeRef::F64 => "float64".into(), - TypeRef::Bool => "bool".into(), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "string".into(), - TypeRef::Bytes | TypeRef::BorrowedBytes => "[]byte".into(), + Ty::I8 => "int8".into(), + Ty::I16 => "int16".into(), + Ty::I32 => "int32".into(), + Ty::U8 => "uint8".into(), + Ty::U16 => "uint16".into(), + Ty::U32 => "uint32".into(), + Ty::U64 => "uint64".into(), + Ty::I64 | Ty::Handle => "int64".into(), + Ty::F32 => "float32".into(), + Ty::F64 => "float64".into(), + Ty::Bool => "bool".into(), + Ty::StringUtf8 | Ty::BorrowedStr => "string".into(), + Ty::Bytes | Ty::BorrowedBytes => "[]byte".into(), // Records are plain value structs; rich enums are sealed interfaces // (nil-able), so neither takes a pointer at the type site. A // cross-module reference (resolved to e.g. `kv.Entry`) must name the // local `Entry` type rather than the qualified `KvEntry`. - TypeRef::Record(n) | TypeRef::RichEnum(n) => go_local(n), - TypeRef::Interface(n) => format!("*{}", go_local(n)), - TypeRef::TypedHandle(n) => format!("*{}", handle_wrapper(n)), - TypeRef::Enum(n) => go_local(n), - TypeRef::Optional(inner) => { + Ty::Record(n) | Ty::RichEnum(n) => go_local(n), + Ty::Interface(n) => format!("*{}", go_local(n)), + Ty::TypedHandle(n) => format!("*{}", handle_wrapper(n)), + Ty::Enum(n) => go_local(n), + Ty::Optional(inner) => { if optional_derefs(inner) { format!("*{}", go_type(inner)) } else { @@ -63,12 +63,11 @@ pub(crate) fn go_type(ty: &TypeRef) -> String { go_type(inner) } } - TypeRef::List(inner) => format!("[]{}", go_type(inner)), + Ty::List(inner) => format!("[]{}", go_type(inner)), // The bare (non-throwing) sequence type; a throwing iterator wrapper // spells `iter.Seq2[T, error]` at its signature site instead. - TypeRef::Iterator(inner) => format!("iter.Seq[{}]", go_type(inner)), - TypeRef::Map(k, v) => format!("map[{}]{}", go_type(k), go_type(v)), - TypeRef::Named(_) => unreachable!("unresolved type reference"), + Ty::Iterator(inner) => format!("iter.Seq[{}]", go_type(inner)), + Ty::Map(k, v) => format!("map[{}]{}", go_type(k), go_type(v)), } } @@ -76,39 +75,39 @@ pub(crate) fn go_type(ty: &TypeRef) -> String { /// when present). Types that are already nil-able (rich enums, slices, maps, /// byte slices, typed handles, interfaces) use nil directly as the none /// marker instead. -pub(crate) fn optional_derefs(inner: &TypeRef) -> bool { +pub(crate) fn optional_derefs(inner: &Ty) -> bool { !matches!( inner, - TypeRef::RichEnum(_) - | TypeRef::List(_) - | TypeRef::Map(_, _) - | TypeRef::Bytes - | TypeRef::BorrowedBytes - | TypeRef::TypedHandle(_) - | TypeRef::Interface(_) + Ty::RichEnum(_) + | Ty::List(_) + | Ty::Map(_, _) + | Ty::Bytes + | Ty::BorrowedBytes + | Ty::TypedHandle(_) + | Ty::Interface(_) ) } /// The Go zero-value expression of a type, returned on the error path of a /// throwing wrapper. -pub(crate) fn go_zero(ty: &TypeRef) -> String { +pub(crate) fn go_zero(ty: &Ty) -> String { match ty { - TypeRef::I8 - | TypeRef::I16 - | TypeRef::I32 - | TypeRef::I64 - | TypeRef::U8 - | TypeRef::U16 - | TypeRef::U32 - | TypeRef::U64 - | TypeRef::Handle - | TypeRef::F32 - | TypeRef::F64 => "0".into(), - TypeRef::Bool => "false".into(), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "\"\"".into(), - TypeRef::Enum(_) => "0".into(), + Ty::I8 + | Ty::I16 + | Ty::I32 + | Ty::I64 + | Ty::U8 + | Ty::U16 + | Ty::U32 + | Ty::U64 + | Ty::Handle + | Ty::F32 + | Ty::F64 => "0".into(), + Ty::Bool => "false".into(), + Ty::StringUtf8 | Ty::BorrowedStr => "\"\"".into(), + Ty::Enum(_) => "0".into(), // A record is a value struct: its zero is the empty literal. - TypeRef::Record(n) => format!("{}{{}}", go_local(n)), + Ty::Record(n) => format!("{}{{}}", go_local(n)), _ => "nil".into(), } } @@ -116,29 +115,29 @@ pub(crate) fn go_zero(ty: &TypeRef) -> String { /// The cgo spelling of a scalar type's C slot (`C.int32_t`, `C._Bool`, /// `C.{prefix}_{module}_{Enum}`), or `None` when the type isn't passed as a /// single scalar slot. -pub(crate) fn c_scalar_type(ty: &TypeRef, prefix: &str, module: &str) -> Option { +pub(crate) fn c_scalar_type(ty: &Ty, prefix: &str, module: &str) -> Option { match ty { - TypeRef::I8 => Some("C.int8_t".into()), - TypeRef::I16 => Some("C.int16_t".into()), - TypeRef::I32 => Some("C.int32_t".into()), - TypeRef::U8 => Some("C.uint8_t".into()), - TypeRef::U16 => Some("C.uint16_t".into()), - TypeRef::U32 => Some("C.uint32_t".into()), - TypeRef::U64 => Some("C.uint64_t".into()), - TypeRef::I64 | TypeRef::Handle => Some("C.int64_t".into()), - TypeRef::F32 => Some("C.float".into()), - TypeRef::F64 => Some("C.double".into()), - TypeRef::Bool => Some("C._Bool".into()), - TypeRef::Enum(n) => Some(format!("C.{}", c_abi_struct_name(n, module, prefix))), + Ty::I8 => Some("C.int8_t".into()), + Ty::I16 => Some("C.int16_t".into()), + Ty::I32 => Some("C.int32_t".into()), + Ty::U8 => Some("C.uint8_t".into()), + Ty::U16 => Some("C.uint16_t".into()), + Ty::U32 => Some("C.uint32_t".into()), + Ty::U64 => Some("C.uint64_t".into()), + Ty::I64 | Ty::Handle => Some("C.int64_t".into()), + Ty::F32 => Some("C.float".into()), + Ty::F64 => Some("C.double".into()), + Ty::Bool => Some("C._Bool".into()), + Ty::Enum(n) => Some(format!("C.{}", c_abi_struct_name(n, module, prefix))), _ => None, } } /// The Go expression converting a Go scalar `expr` into its C slot value. /// Non-scalar types pass through unchanged. -pub(crate) fn c_scalar_conv(expr: &str, ty: &TypeRef, prefix: &str, module: &str) -> String { +pub(crate) fn c_scalar_conv(expr: &str, ty: &Ty, prefix: &str, module: &str) -> String { match ty { - TypeRef::Bool => format!("boolToC({expr})"), + Ty::Bool => format!("boolToC({expr})"), _ => { if let Some(ct) = c_scalar_type(ty, prefix, module) { format!("{ct}({expr})") @@ -151,30 +150,30 @@ pub(crate) fn c_scalar_conv(expr: &str, ty: &TypeRef, prefix: &str, module: &str /// The Go expression converting a C scalar `expr` back into its Go value. /// Non-scalar types pass through unchanged. -pub(crate) fn go_scalar_conv(expr: &str, ty: &TypeRef) -> String { +pub(crate) fn go_scalar_conv(expr: &str, ty: &Ty) -> String { match ty { - TypeRef::I8 => format!("int8({expr})"), - TypeRef::I16 => format!("int16({expr})"), - TypeRef::I32 => format!("int32({expr})"), - TypeRef::U8 => format!("uint8({expr})"), - TypeRef::U16 => format!("uint16({expr})"), - TypeRef::U32 => format!("uint32({expr})"), - TypeRef::U64 => format!("uint64({expr})"), - TypeRef::I64 | TypeRef::Handle => format!("int64({expr})"), - TypeRef::F32 => format!("float32({expr})"), - TypeRef::F64 => format!("float64({expr})"), - TypeRef::Bool => format!("cToBool({expr})"), - TypeRef::Enum(n) => format!("{}({expr})", go_local(n)), + Ty::I8 => format!("int8({expr})"), + Ty::I16 => format!("int16({expr})"), + Ty::I32 => format!("int32({expr})"), + Ty::U8 => format!("uint8({expr})"), + Ty::U16 => format!("uint16({expr})"), + Ty::U32 => format!("uint32({expr})"), + Ty::U64 => format!("uint64({expr})"), + Ty::I64 | Ty::Handle => format!("int64({expr})"), + Ty::F32 => format!("float32({expr})"), + Ty::F64 => format!("float64({expr})"), + Ty::Bool => format!("cToBool({expr})"), + Ty::Enum(n) => format!("{}({expr})", go_local(n)), _ => expr.to_string(), } } /// The Go expression wrapping an opaque C pointer (`ptr_expr`) into the /// wrapper type for an interface or typed-handle reference. -pub(crate) fn go_wrap_expr(ty: &TypeRef, ptr_expr: &str) -> String { +pub(crate) fn go_wrap_expr(ty: &Ty, ptr_expr: &str) -> String { match ty { - TypeRef::Interface(n) => format!("&{}{{ptr: {ptr_expr}}}", go_local(n)), - TypeRef::TypedHandle(n) => format!("&{}{{ptr: {ptr_expr}}}", handle_wrapper(n)), + Ty::Interface(n) => format!("&{}{{ptr: {ptr_expr}}}", go_local(n)), + Ty::TypedHandle(n) => format!("&{}{{ptr: {ptr_expr}}}", handle_wrapper(n)), _ => unreachable!("only interfaces and typed handles wrap C pointers"), } } diff --git a/crates/weaveffi-gen-node/Cargo.toml b/crates/weaveffi-gen-node/Cargo.toml index c4e9adf5..ab34e0ed 100644 --- a/crates/weaveffi-gen-node/Cargo.toml +++ b/crates/weaveffi-gen-node/Cargo.toml @@ -11,13 +11,9 @@ categories = ["development-tools::ffi"] [dependencies] weaveffi-core = { path = "../weaveffi-core", version = "0.21.0" } -weaveffi-ir = { path = "../weaveffi-ir", version = "0.21.0" } camino = { workspace = true } heck = { workspace = true } serde = { workspace = true } -[dev-dependencies] -tempfile = { workspace = true } - [lints] workspace = true diff --git a/crates/weaveffi-gen-node/src/addon.rs b/crates/weaveffi-gen-node/src/addon.rs index e1c619f5..30b0f928 100644 --- a/crates/weaveffi-gen-node/src/addon.rs +++ b/crates/weaveffi-gen-node/src/addon.rs @@ -8,13 +8,13 @@ use weaveffi_core::abi; use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ iterator_item_ctype, BindingModel, CallShape, CallbackBinding, FnBinding, InterfaceBinding, IteratorBinding, ListenerBinding, ParamBinding, }; use weaveffi_core::plan::{self, elem_free, ArgPass, ElemFree, RetPass}; use weaveffi_core::utils::{render_prelude, render_trailer, wrapper_name, CommentStyle}; -use weaveffi_ir::ir::TypeRef; use crate::runtime::model_has_iterators; use crate::types::{iface_member_base, js_fn_name}; @@ -22,45 +22,43 @@ use crate::types::{iface_member_base, js_fn_name}; /// The C return-type spelling of `ty` at a call site. Buffered values render /// as `const uint8_t*` (the encoded buffer); an iterator launcher's handle is /// held as `void*` so the shared state cell can adopt it. -fn c_ret_type_str(ty: &TypeRef, module: &str, prefix: &str) -> String { - if matches!(ty, TypeRef::Iterator(_)) { +fn c_ret_type_str(ty: &Ty, module: &str, prefix: &str) -> String { + if matches!(ty, Ty::Iterator(_)) { return "void*".into(); } abi::lower_return(ty, module).ret.render_c(prefix) } /// The bare C type of a scalar (or C-enum-free leaf) parameter temporary. -fn c_scalar_type(ty: &TypeRef) -> &'static str { +fn c_scalar_type(ty: &Ty) -> &'static str { match ty { - TypeRef::I8 => "int8_t", - TypeRef::I16 => "int16_t", - TypeRef::I32 => "int32_t", - TypeRef::I64 => "int64_t", - TypeRef::U8 => "uint8_t", - TypeRef::U16 => "uint16_t", - TypeRef::U32 => "uint32_t", - TypeRef::U64 => "uint64_t", - TypeRef::F32 => "float", - TypeRef::F64 => "double", - TypeRef::Bool => "bool", + Ty::I8 => "int8_t", + Ty::I16 => "int16_t", + Ty::I32 => "int32_t", + Ty::I64 => "int64_t", + Ty::U8 => "uint8_t", + Ty::U16 => "uint16_t", + Ty::U32 => "uint32_t", + Ty::U64 => "uint64_t", + Ty::F32 => "float", + Ty::F64 => "double", + Ty::Bool => "bool", _ => unreachable!("not a scalar type"), } } /// The N-API getter that reads one direct-slot JS argument. -fn napi_getter(ty: &TypeRef) -> &'static str { +fn napi_getter(ty: &Ty) -> &'static str { match ty { // i8/i16 are read through the 32-bit signed getter (N-API has no // narrower int getter) and narrowed at the use site. - TypeRef::I8 | TypeRef::I16 | TypeRef::I32 | TypeRef::Enum(_) => "napi_get_value_int32", - TypeRef::U8 | TypeRef::U16 | TypeRef::U32 => "napi_get_value_uint32", + Ty::I8 | Ty::I16 | Ty::I32 | Ty::Enum(_) => "napi_get_value_int32", + Ty::U8 | Ty::U16 | Ty::U32 => "napi_get_value_uint32", // u64 mirrors i64/handle: read as a 64-bit int, reinterpreted as needed. - TypeRef::I64 | TypeRef::U64 | TypeRef::Handle | TypeRef::TypedHandle(_) => { - "napi_get_value_int64" - } + Ty::I64 | Ty::U64 | Ty::Handle | Ty::TypedHandle(_) => "napi_get_value_int64", // f32 is read as a double then narrowed to float at the use site. - TypeRef::F32 | TypeRef::F64 => "napi_get_value_double", - TypeRef::Bool => "napi_get_value_bool", + Ty::F32 | Ty::F64 => "napi_get_value_double", + Ty::Bool => "napi_get_value_bool", _ => "napi_get_value_int64", } } @@ -70,12 +68,12 @@ fn napi_getter(ty: &TypeRef) -> &'static str { /// and `double` getters, so `i8/i16/u8/u16/f32` must be read into a wider /// temporary and then narrowed with an explicit cast to the real ABI type; /// `u64` is read as `int64_t` then reinterpreted. -fn napi_read_tmp_type(ty: &TypeRef) -> &'static str { +fn napi_read_tmp_type(ty: &Ty) -> &'static str { match ty { - TypeRef::I8 | TypeRef::I16 => "int32_t", - TypeRef::U8 | TypeRef::U16 => "uint32_t", - TypeRef::U64 => "int64_t", - TypeRef::F32 => "double", + Ty::I8 | Ty::I16 => "int32_t", + Ty::U8 | Ty::U16 => "uint32_t", + Ty::U64 => "int64_t", + Ty::F32 => "double", _ => "int64_t", } } @@ -314,7 +312,7 @@ pub(crate) fn render_addon_c( let prefix = model.prefix.as_str(); let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); out.push_str(&format!( - "#include \n#include \"{prefix}.h\"\n#include \n#include \n\n" + "#include \n#include \"{prefix}.h\"\n#include \n#include \n#include \n\n" )); let mut all_exports: Vec<(String, String)> = Vec::new(); @@ -435,6 +433,23 @@ pub(crate) fn render_addon_c( } out.push_str("static napi_value Init(napi_env env, napi_value exports) {\n"); + // The addon links the producer directly, so a missing symbol already + // fails at load; this catches a producer built for a different revision. + out.push_str(" {\n"); + out.push_str(&format!(" uint32_t found = {prefix}_abi_version();\n")); + out.push_str(&format!( + " if (found != {upper}_ABI_VERSION) {{\n", + upper = prefix.to_uppercase() + )); + out.push_str(" char msg[160];\n"); + out.push_str(&format!( + " snprintf(msg, sizeof msg, \"WeaveFFI ABI mismatch: these bindings expect revision %u but the loaded library reports revision %u\", (unsigned){upper}_ABI_VERSION, (unsigned)found);\n", + upper = prefix.to_uppercase() + )); + out.push_str(" napi_throw_error(env, NULL, msg);\n"); + out.push_str(" return NULL;\n"); + out.push_str(" }\n"); + out.push_str(" }\n"); if !all_exports.is_empty() { out.push_str(" napi_property_descriptor props[] = {\n"); for (js_name, napi_fn) in &all_exports { @@ -542,7 +557,7 @@ fn render_cb_payload_struct(out: &mut String, cb: &CallbackBinding, prefix: &str w.line(format!("void* {n0};")); } ArgPass::Direct { slot } => { - if matches!(p.ty, TypeRef::TypedHandle(_)) { + if matches!(p.ty, Ty::TypedHandle(_)) { w.line(format!("void* {n0};")); } else { w.line(format!("{} {n0};", slot.ty.render_c(prefix))); @@ -597,7 +612,7 @@ fn render_cb_tramp(out: &mut String, cb: &CallbackBinding, prefix: &str) { out.push_str(&format!(" p->{n0} = (void*){n0};\n")); } ArgPass::Direct { .. } => { - if matches!(p.ty, TypeRef::TypedHandle(_)) { + if matches!(p.ty, Ty::TypedHandle(_)) { out.push_str(&format!(" p->{n0} = (void*){n0};\n")); } else { out.push_str(&format!(" p->{n0} = {n0};\n")); @@ -628,7 +643,7 @@ fn emit_payload_to_napi(out: &mut String, p: &ParamBinding, idx: usize) { " if (p->{n0}) napi_create_int64(env, (int64_t)(intptr_t)p->{n0}, &{target}); else napi_get_null(env, &{target});\n" )), ArgPass::Direct { .. } => { - if matches!(p.ty, TypeRef::TypedHandle(_)) { + if matches!(p.ty, Ty::TypedHandle(_)) { out.push_str(&format!( " napi_create_int64(env, (int64_t)(intptr_t)p->{n0}, &{target});\n" )); @@ -641,19 +656,19 @@ fn emit_payload_to_napi(out: &mut String, p: &ParamBinding, idx: usize) { } /// One scalar-ish payload value to a `napi_value` (single statement). -fn payload_leaf_to_napi(ty: &TypeRef, expr: &str, target: &str) -> String { +fn payload_leaf_to_napi(ty: &Ty, expr: &str, target: &str) -> String { match ty { - TypeRef::I32 => format!("napi_create_int32(env, {expr}, &{target});"), - TypeRef::U32 => format!("napi_create_uint32(env, {expr}, &{target});"), - TypeRef::I64 => format!("napi_create_int64(env, {expr}, &{target});"), - TypeRef::F64 => format!("napi_create_double(env, {expr}, &{target});"), - TypeRef::I8 | TypeRef::I16 => format!("napi_create_int32(env, {expr}, &{target});"), - TypeRef::U8 | TypeRef::U16 => format!("napi_create_uint32(env, {expr}, &{target});"), - TypeRef::U64 => format!("napi_create_int64(env, (int64_t){expr}, &{target});"), - TypeRef::F32 => format!("napi_create_double(env, {expr}, &{target});"), - TypeRef::Bool => format!("napi_get_boolean(env, {expr}, &{target});"), - TypeRef::Handle => format!("napi_create_int64(env, (int64_t){expr}, &{target});"), - TypeRef::Enum(_) => format!("napi_create_int32(env, (int32_t){expr}, &{target});"), + Ty::I32 => format!("napi_create_int32(env, {expr}, &{target});"), + Ty::U32 => format!("napi_create_uint32(env, {expr}, &{target});"), + Ty::I64 => format!("napi_create_int64(env, {expr}, &{target});"), + Ty::F64 => format!("napi_create_double(env, {expr}, &{target});"), + Ty::I8 | Ty::I16 => format!("napi_create_int32(env, {expr}, &{target});"), + Ty::U8 | Ty::U16 => format!("napi_create_uint32(env, {expr}, &{target});"), + Ty::U64 => format!("napi_create_int64(env, (int64_t){expr}, &{target});"), + Ty::F32 => format!("napi_create_double(env, {expr}, &{target});"), + Ty::Bool => format!("napi_get_boolean(env, {expr}, &{target});"), + Ty::Handle => format!("napi_create_int64(env, (int64_t){expr}, &{target});"), + Ty::Enum(_) => format!("napi_create_int32(env, (int32_t){expr}, &{target});"), _ => format!("napi_get_null(env, &{target});"), } } @@ -799,8 +814,8 @@ enum AsyncResultShape { /// they cross as owned pointers the callback adopts, which [`plan::ret_pass`] /// does not distinguish from by-value returns (and iterator returns have no /// `RetPass` at all). -fn async_result_shape(ret: Option<&TypeRef>, module: &str, prefix: &str) -> AsyncResultShape { - if let Some(TypeRef::TypedHandle(_) | TypeRef::Iterator(_)) = ret { +fn async_result_shape(ret: Option<&Ty>, module: &str, prefix: &str) -> AsyncResultShape { + if let Some(Ty::TypedHandle(_) | Ty::Iterator(_)) = ret { return AsyncResultShape::Object; } match plan::ret_pass(ret, module, prefix) { @@ -816,7 +831,7 @@ fn async_result_shape(ret: Option<&TypeRef>, module: &str, prefix: &str) -> Asyn /// The `, ` suffix of an async completion callback's result /// slots, rendered from the shared ABI lowering so the signature matches the /// producer's typedef exactly. -fn async_cb_result_params_node(ret: Option<&TypeRef>, module: &str, prefix: &str) -> String { +fn async_cb_result_params_node(ret: Option<&Ty>, module: &str, prefix: &str) -> String { match ret { None => String::new(), Some(ty) => abi::callback_result_params(ty, module) @@ -958,34 +973,22 @@ fn render_async_machinery( match &shape { AsyncResultShape::None => out.push_str(" napi_get_undefined(env, &val);\n"), AsyncResultShape::Value => match f.ret.as_ref() { - Some(TypeRef::I32) => { - out.push_str(" napi_create_int32(env, ctx->result, &val);\n") - } - Some(TypeRef::U32) => { - out.push_str(" napi_create_uint32(env, ctx->result, &val);\n") - } - Some(TypeRef::I64) => { - out.push_str(" napi_create_int64(env, ctx->result, &val);\n") - } - Some(TypeRef::F64) => { - out.push_str(" napi_create_double(env, ctx->result, &val);\n") - } - Some(TypeRef::I8 | TypeRef::I16) => { + Some(Ty::I32) => out.push_str(" napi_create_int32(env, ctx->result, &val);\n"), + Some(Ty::U32) => out.push_str(" napi_create_uint32(env, ctx->result, &val);\n"), + Some(Ty::I64) => out.push_str(" napi_create_int64(env, ctx->result, &val);\n"), + Some(Ty::F64) => out.push_str(" napi_create_double(env, ctx->result, &val);\n"), + Some(Ty::I8 | Ty::I16) => { out.push_str(" napi_create_int32(env, ctx->result, &val);\n"); } - Some(TypeRef::U8 | TypeRef::U16) => { + Some(Ty::U8 | Ty::U16) => { out.push_str(" napi_create_uint32(env, ctx->result, &val);\n"); } - Some(TypeRef::U64 | TypeRef::Handle) => { + Some(Ty::U64 | Ty::Handle) => { out.push_str(" napi_create_int64(env, (int64_t)ctx->result, &val);\n"); } - Some(TypeRef::F32) => { - out.push_str(" napi_create_double(env, ctx->result, &val);\n") - } - Some(TypeRef::Bool) => { - out.push_str(" napi_get_boolean(env, ctx->result, &val);\n") - } - Some(TypeRef::Enum(_)) => { + Some(Ty::F32) => out.push_str(" napi_create_double(env, ctx->result, &val);\n"), + Some(Ty::Bool) => out.push_str(" napi_get_boolean(env, ctx->result, &val);\n"), + Some(Ty::Enum(_)) => { out.push_str(" napi_create_int32(env, (int32_t)ctx->result, &val);\n"); } _ => unreachable!("value shape covers scalars, bools, enums, and handles"), @@ -1004,7 +1007,7 @@ fn render_async_machinery( // A nullable interface result resolves `null` for the absent // case; every other object pointer is surfaced as the raw // handle the JS class adopts. - if matches!(f.ret.as_ref(), Some(TypeRef::Optional(_))) { + if matches!(f.ret.as_ref(), Some(Ty::Optional(_))) { out.push_str( " if (ctx->result == NULL) napi_get_null(env, &val); else napi_create_int64(env, (int64_t)(intptr_t)ctx->result, &val);\n", ); @@ -1244,7 +1247,7 @@ fn emit_param( } } ArgPass::Direct { slot } => match &p.ty { - TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Bool => { + Ty::I32 | Ty::U32 | Ty::I64 | Ty::F64 | Ty::Bool => { let ct = c_scalar_type(&p.ty); let getter = napi_getter(&p.ty); out.push_str(&format!(" {ct} {name};\n")); @@ -1253,12 +1256,7 @@ fn emit_param( } // N-API has no narrower-than-32-bit / float getter, so read into a // correctly-sized temporary and narrow to the real ABI type. - TypeRef::I8 - | TypeRef::I16 - | TypeRef::U8 - | TypeRef::U16 - | TypeRef::U64 - | TypeRef::F32 => { + Ty::I8 | Ty::I16 | Ty::U8 | Ty::U16 | Ty::U64 | Ty::F32 => { let ct = c_scalar_type(&p.ty); let getter = napi_getter(&p.ty); let raw = napi_read_tmp_type(&p.ty); @@ -1269,14 +1267,14 @@ fn emit_param( // The untyped handle keeps the runtime's literal type name; the // runtime types stay `weaveffi_*` regardless of the configured // business-symbol prefix. - TypeRef::Handle => { + Ty::Handle => { out.push_str(&format!(" int64_t {name}_raw;\n")); out.push_str(&format!( " napi_get_value_int64(env, args[{idx}], &{name}_raw);\n" )); c_args.push(format!("(weaveffi_handle_t){name}_raw")); } - TypeRef::TypedHandle(_) => { + Ty::TypedHandle(_) => { let ptr_ty = slot.ty.render_c(prefix); out.push_str(&format!(" int64_t {name}_raw;\n")); out.push_str(&format!( @@ -1284,7 +1282,7 @@ fn emit_param( )); c_args.push(format!("({ptr_ty})(intptr_t){name}_raw")); } - TypeRef::Enum(_) => { + Ty::Enum(_) => { let etype = slot.ty.render_c(prefix); out.push_str(&format!(" int32_t {name};\n")); out.push_str(&format!( @@ -1303,11 +1301,11 @@ fn emit_param( fn emit_ret_out_params( out: &mut String, c_args: &mut Vec, - ty: &TypeRef, + ty: &Ty, module: &str, prefix: &str, ) { - if matches!(ty, TypeRef::Iterator(_)) { + if matches!(ty, Ty::Iterator(_)) { return; } if matches!( @@ -1321,19 +1319,19 @@ fn emit_ret_out_params( /// The C statement that creates a napi value `target` from a leaf C expression /// `expr` (scalars, bools, enums, handles). -fn napi_create_leaf(env: &str, ty: &TypeRef, expr: &str, target: &str) -> String { +fn napi_create_leaf(env: &str, ty: &Ty, expr: &str, target: &str) -> String { match ty { - TypeRef::I32 => format!("napi_create_int32({env}, {expr}, &{target});"), - TypeRef::U32 => format!("napi_create_uint32({env}, {expr}, &{target});"), - TypeRef::I64 => format!("napi_create_int64({env}, {expr}, &{target});"), - TypeRef::F64 => format!("napi_create_double({env}, {expr}, &{target});"), - TypeRef::I8 | TypeRef::I16 => format!("napi_create_int32({env}, {expr}, &{target});"), - TypeRef::U8 | TypeRef::U16 => format!("napi_create_uint32({env}, {expr}, &{target});"), - TypeRef::U64 => format!("napi_create_int64({env}, (int64_t)({expr}), &{target});"), - TypeRef::F32 => format!("napi_create_double({env}, {expr}, &{target});"), - TypeRef::Bool => format!("napi_get_boolean({env}, {expr}, &{target});"), - TypeRef::Enum(_) => format!("napi_create_int32({env}, (int32_t)({expr}), &{target});"), - TypeRef::Handle | TypeRef::TypedHandle(_) => { + Ty::I32 => format!("napi_create_int32({env}, {expr}, &{target});"), + Ty::U32 => format!("napi_create_uint32({env}, {expr}, &{target});"), + Ty::I64 => format!("napi_create_int64({env}, {expr}, &{target});"), + Ty::F64 => format!("napi_create_double({env}, {expr}, &{target});"), + Ty::I8 | Ty::I16 => format!("napi_create_int32({env}, {expr}, &{target});"), + Ty::U8 | Ty::U16 => format!("napi_create_uint32({env}, {expr}, &{target});"), + Ty::U64 => format!("napi_create_int64({env}, (int64_t)({expr}), &{target});"), + Ty::F32 => format!("napi_create_double({env}, {expr}, &{target});"), + Ty::Bool => format!("napi_get_boolean({env}, {expr}, &{target});"), + Ty::Enum(_) => format!("napi_create_int32({env}, (int32_t)({expr}), &{target});"), + Ty::Handle | Ty::TypedHandle(_) => { format!("napi_create_int64({env}, (int64_t)(intptr_t)({expr}), &{target});") } _ => format!("napi_get_null({env}, &{target});"), @@ -1345,9 +1343,9 @@ fn napi_create_leaf(env: &str, ty: &TypeRef, expr: &str, target: &str) -> String /// receiving plan. A buffered return is copied into a JS `Buffer` and /// released with `weaveffi_free_bytes`; the JS loader decodes it into the /// idiomatic value. -fn emit_ret_to_napi(out: &mut String, ty: &TypeRef, module: &str, prefix: &str, f: &FnBinding) { +fn emit_ret_to_napi(out: &mut String, ty: &Ty, module: &str, prefix: &str, f: &FnBinding) { out.push_str(" napi_value ret;\n"); - if matches!(ty, TypeRef::Iterator(_)) { + if matches!(ty, Ty::Iterator(_)) { // Lazy: the launcher's owned iterator handle is boxed into a // heap-allocated state cell and wrapped in a JS external. The // JS wrapper drives the per-iterator `next`/`destroy` entry @@ -1393,22 +1391,20 @@ fn emit_ret_to_napi(out: &mut String, ty: &TypeRef, module: &str, prefix: &str, } } RetPass::Direct => match ty { - TypeRef::I32 => out.push_str(" napi_create_int32(env, result, &ret);\n"), - TypeRef::U32 => out.push_str(" napi_create_uint32(env, result, &ret);\n"), - TypeRef::I64 => out.push_str(" napi_create_int64(env, result, &ret);\n"), - TypeRef::F64 => out.push_str(" napi_create_double(env, result, &ret);\n"), - TypeRef::I8 | TypeRef::I16 => out.push_str(" napi_create_int32(env, result, &ret);\n"), - TypeRef::U8 | TypeRef::U16 => { - out.push_str(" napi_create_uint32(env, result, &ret);\n") - } - TypeRef::U64 => out.push_str(" napi_create_int64(env, (int64_t)result, &ret);\n"), - TypeRef::F32 => out.push_str(" napi_create_double(env, result, &ret);\n"), - TypeRef::Bool => out.push_str(" napi_get_boolean(env, result, &ret);\n"), - TypeRef::Enum(_) => { + Ty::I32 => out.push_str(" napi_create_int32(env, result, &ret);\n"), + Ty::U32 => out.push_str(" napi_create_uint32(env, result, &ret);\n"), + Ty::I64 => out.push_str(" napi_create_int64(env, result, &ret);\n"), + Ty::F64 => out.push_str(" napi_create_double(env, result, &ret);\n"), + Ty::I8 | Ty::I16 => out.push_str(" napi_create_int32(env, result, &ret);\n"), + Ty::U8 | Ty::U16 => out.push_str(" napi_create_uint32(env, result, &ret);\n"), + Ty::U64 => out.push_str(" napi_create_int64(env, (int64_t)result, &ret);\n"), + Ty::F32 => out.push_str(" napi_create_double(env, result, &ret);\n"), + Ty::Bool => out.push_str(" napi_get_boolean(env, result, &ret);\n"), + Ty::Enum(_) => { out.push_str(" napi_create_int32(env, (int32_t)result, &ret);\n"); } // Handles are opaque tokens surfaced as int64. - TypeRef::Handle | TypeRef::TypedHandle(_) => { + Ty::Handle | Ty::TypedHandle(_) => { out.push_str(" napi_create_int64(env, (int64_t)(intptr_t)result, &ret);\n"); } other => unreachable!("direct return with non-direct type {other:?}"), diff --git a/crates/weaveffi-gen-node/src/codec.rs b/crates/weaveffi-gen-node/src/codec.rs index 41d52b2f..7cfadc58 100644 --- a/crates/weaveffi-gen-node/src/codec.rs +++ b/crates/weaveffi-gen-node/src/codec.rs @@ -10,13 +10,12 @@ //! user-codec shape) live in `weaveffi-core`, not here. The composition is //! fixed at generation time from the IR; no runtime type dispatch happens. -use weaveffi_core::abi::is_buffered; use weaveffi_core::codegen::CodeWriter; use weaveffi_core::errors::ERROR_BRAND; +use weaveffi_core::model::Ty; use weaveffi_core::model::{BindingModel, ModuleBinding}; +use weaveffi_core::model::{Prim, WireType}; use weaveffi_core::utils::local_type_name; -use weaveffi_core::wire::{self, WireType}; -use weaveffi_ir::ir::TypeRef; /// The reader/writer method of the private buffer runtime for a scalar wire /// shape (including strings and bytes, whose length-prefixed reads live in @@ -28,19 +27,19 @@ use weaveffi_ir::ir::TypeRef; /// codecs, and containers before falling through to a scalar method. fn scalar_method(wt: WireType<'_>) -> &'static str { match wt { - WireType::Bool => "bool", - WireType::I8 => "i8", - WireType::U8 => "u8", - WireType::I16 => "i16", - WireType::U16 => "u16", - WireType::I32 => "i32", - WireType::U32 => "u32", - WireType::I64 => "i64", - WireType::U64 => "u64", - WireType::F32 => "f32", - WireType::F64 => "f64", - WireType::String => "str", - WireType::Bytes => "bytes", + WireType::Prim(Prim::Bool) => "bool", + WireType::Prim(Prim::I8) => "i8", + WireType::Prim(Prim::U8) => "u8", + WireType::Prim(Prim::I16) => "i16", + WireType::Prim(Prim::U16) => "u16", + WireType::Prim(Prim::I32) => "i32", + WireType::Prim(Prim::U32) => "u32", + WireType::Prim(Prim::I64) => "i64", + WireType::Prim(Prim::U64) => "u64", + WireType::Prim(Prim::F32) => "f32", + WireType::Prim(Prim::F64) => "f64", + WireType::Prim(Prim::String) => "str", + WireType::Prim(Prim::Bytes) => "bytes", other => unreachable!("composite wire shape dispatched by the caller: {other:?}"), } } @@ -48,10 +47,10 @@ fn scalar_method(wt: WireType<'_>) -> &'static str { /// A JS function expression `(w, v) => void` writing one value of `ty` in the /// wire format. Records and rich enums name their generated pack function; /// optionals, lists, and maps compose the generic combinators. -pub(crate) fn js_writer_fn(ty: &TypeRef) -> String { - match wire::classify(ty) { +pub(crate) fn js_writer_fn(ty: &Ty) -> String { + match ty.wire() { WireType::Enum(_) => "(w, v) => w.i32(v)".into(), - WireType::Handle => "(w, v) => w.u64(v)".into(), + WireType::Handle(_) => "(w, v) => w.u64(v)".into(), WireType::User(n) => format!("__pack{}", local_type_name(n)), WireType::Optional(inner) => format!("(w, v) => __wOpt(w, v, {})", js_writer_fn(inner)), WireType::List(inner) => format!("(w, v) => __wList(w, v, {})", js_writer_fn(inner)), @@ -67,12 +66,12 @@ pub(crate) fn js_writer_fn(ty: &TypeRef) -> String { /// A JS function expression `(w, k) => void` writing one *map key*. JS object /// keys arrive as strings from `Object.keys`, so numeric key types coerce /// through `Number` (or `BigInt`, inside the 64-bit writer methods) first. -fn js_map_key_writer_fn(ty: &TypeRef) -> String { - match wire::classify(ty) { - WireType::String => "(w, k) => w.str(k)".into(), - WireType::Bool => "(w, k) => w.bool(k === true || k === 'true')".into(), - WireType::I64 => "(w, k) => w.i64(k)".into(), - WireType::U64 | WireType::Handle => "(w, k) => w.u64(k)".into(), +fn js_map_key_writer_fn(ty: &Ty) -> String { + match ty.wire() { + WireType::Prim(Prim::String) => "(w, k) => w.str(k)".into(), + WireType::Prim(Prim::Bool) => "(w, k) => w.bool(k === true || k === 'true')".into(), + WireType::Prim(Prim::I64) => "(w, k) => w.i64(k)".into(), + WireType::Prim(Prim::U64) | WireType::Handle(_) => "(w, k) => w.u64(k)".into(), WireType::Enum(_) => "(w, k) => w.i32(Number(k))".into(), leaf => format!("(w, k) => w.{}(Number(k))", scalar_method(leaf)), } @@ -82,15 +81,13 @@ fn js_map_key_writer_fn(ty: &TypeRef) -> String { /// wire format. 64-bit integers surface as numbers (matching the TS surface); /// handles surface as `BigInt`s except typed handles, which keep the numeric /// handle spelling the addon uses. -pub(crate) fn js_reader_fn(ty: &TypeRef) -> String { - match wire::classify(ty) { - WireType::I64 => "(r) => Number(r.i64())".into(), - WireType::U64 => "(r) => Number(r.u64())".into(), +pub(crate) fn js_reader_fn(ty: &Ty) -> String { + match ty.wire() { + WireType::Prim(Prim::I64) => "(r) => Number(r.i64())".into(), + WireType::Prim(Prim::U64) => "(r) => Number(r.u64())".into(), // Both handle kinds share the u64 token; only the JS surface differs. - WireType::Handle if matches!(ty, TypeRef::TypedHandle(_)) => { - "(r) => Number(r.u64())".into() - } - WireType::Handle => "(r) => r.u64()".into(), + WireType::Handle(_) if matches!(ty, Ty::TypedHandle(_)) => "(r) => Number(r.u64())".into(), + WireType::Handle(_) => "(r) => r.u64()".into(), WireType::Enum(_) => "(r) => r.i32()".into(), WireType::User(n) => format!("__unpack{}", local_type_name(n)), WireType::Optional(inner) => format!("(r) => __rOpt(r, {})", js_reader_fn(inner)), @@ -105,10 +102,10 @@ pub(crate) fn js_reader_fn(ty: &TypeRef) -> String { /// The JS statement expression writing `val` of type `ty` onto writer `w`. /// Direct spellings for leaves and generated pack functions; combinator calls /// for optionals, lists, and maps. -fn js_write_expr(ty: &TypeRef, val: &str) -> String { - match wire::classify(ty) { +fn js_write_expr(ty: &Ty, val: &str) -> String { + match ty.wire() { WireType::Enum(_) => format!("w.i32({val})"), - WireType::Handle => format!("w.u64({val})"), + WireType::Handle(_) => format!("w.u64({val})"), WireType::User(n) => format!("__pack{}(w, {val})", local_type_name(n)), WireType::Optional(inner) => format!("__wOpt(w, {val}, {})", js_writer_fn(inner)), WireType::List(inner) => format!("__wList(w, {val}, {})", js_writer_fn(inner)), @@ -122,13 +119,13 @@ fn js_write_expr(ty: &TypeRef, val: &str) -> String { } /// The JS expression reading one value of type `ty` from reader `r`. -pub(crate) fn js_read_expr(ty: &TypeRef) -> String { - match wire::classify(ty) { - WireType::I64 => "Number(r.i64())".into(), - WireType::U64 => "Number(r.u64())".into(), +pub(crate) fn js_read_expr(ty: &Ty) -> String { + match ty.wire() { + WireType::Prim(Prim::I64) => "Number(r.i64())".into(), + WireType::Prim(Prim::U64) => "Number(r.u64())".into(), // As in [`js_reader_fn`]: one wire token, two JS spellings. - WireType::Handle if matches!(ty, TypeRef::TypedHandle(_)) => "Number(r.u64())".into(), - WireType::Handle => "r.u64()".into(), + WireType::Handle(_) if matches!(ty, Ty::TypedHandle(_)) => "Number(r.u64())".into(), + WireType::Handle(_) => "r.u64()".into(), WireType::Enum(_) => "r.i32()".into(), WireType::User(n) => format!("__unpack{}(r)", local_type_name(n)), WireType::Optional(inner) => format!("__rOpt(r, {})", js_reader_fn(inner)), @@ -150,12 +147,11 @@ pub(crate) fn model_uses_buffers(model: &BindingModel) -> bool { .is_some_and(|e| e.declared_here && e.codes.iter().any(|c| !c.fields.is_empty())) || m.callbacks .iter() - .any(|cb| cb.params.iter().any(|p| is_buffered(&p.ty))) + .any(|cb| cb.params.iter().any(|p| p.ty.is_buffered())) || m.callables().any(|f| { - f.params.iter().any(|p| is_buffered(&p.ty)) + f.params.iter().any(|p| p.ty.is_buffered()) || f.ret.as_ref().is_some_and(|t| { - is_buffered(t) - || matches!(t, TypeRef::Iterator(inner) if is_buffered(inner)) + t.is_buffered() || matches!(t, Ty::Iterator(inner) if inner.is_buffered()) }) }) }) diff --git a/crates/weaveffi-gen-node/src/entities.rs b/crates/weaveffi-gen-node/src/entities.rs index 1cc71742..eb5b73c4 100644 --- a/crates/weaveffi-gen-node/src/entities.rs +++ b/crates/weaveffi-gen-node/src/entities.rs @@ -3,9 +3,9 @@ //! assembler that composes them with the runtime prelude. use heck::ToLowerCamelCase; -use weaveffi_core::abi::is_buffered; use weaveffi_core::codegen::CodeWriter; use weaveffi_core::errors::{type_name as error_type_name, ERROR_BRAND}; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ BindingModel, ErrorBinding, FnBinding, InterfaceBinding, ListenerBinding, ModuleBinding, ParamBinding, @@ -14,7 +14,6 @@ use weaveffi_core::plan::ArgPass; use weaveffi_core::utils::{ local_type_name, render_prelude, render_trailer, wrapper_name, CommentStyle, }; -use weaveffi_ir::ir::TypeRef; use crate::codec::{ js_read_expr, js_reader_fn, js_writer_fn, model_uses_buffers, render_pack_fns_js, @@ -36,10 +35,10 @@ struct RetWrap { } /// Recognize a class-typed (interface) return, direct or optional. -fn js_ret_wrap(ret: Option<&TypeRef>) -> Option { - fn direct(ty: &TypeRef, optional: bool) -> Option { +fn js_ret_wrap(ret: Option<&Ty>) -> Option { + fn direct(ty: &Ty, optional: bool) -> Option { match ty { - TypeRef::Interface(n) => Some(RetWrap { + Ty::Interface(n) => Some(RetWrap { cls: local_type_name(n).to_string(), optional, }), @@ -47,7 +46,7 @@ fn js_ret_wrap(ret: Option<&TypeRef>) -> Option { } } match ret? { - TypeRef::Optional(inner) => direct(inner, true), + Ty::Optional(inner) => direct(inner, true), ty => direct(ty, false), } } @@ -61,9 +60,9 @@ fn js_arg_expr(js_name: &str, p: &ParamBinding) -> String { ArgPass::Buffer { .. } => format!("__encode({}, {js_name})", js_writer_fn(&p.ty)), ArgPass::Object { .. } => { let cls = match &p.ty { - TypeRef::Interface(n) => local_type_name(n), - TypeRef::Optional(inner) => match inner.as_ref() { - TypeRef::Interface(n) => local_type_name(n), + Ty::Interface(n) => local_type_name(n), + Ty::Optional(inner) => match inner.as_ref() { + Ty::Interface(n) => local_type_name(n), other => unreachable!("non-interface optional is buffered: {other:?}"), }, other => unreachable!("object-passed parameter with type {other:?}"), @@ -220,11 +219,11 @@ fn emit_wrapper_body_js( }; let call = format!("{invoke}(addon.{addon_name}, [{args}], {map_expr})"); - if let Some(TypeRef::Iterator(inner)) = f.ret.as_ref() { + if let Some(Ty::Iterator(inner)) = f.ret.as_ref() { // Launch, then wrap the external in the lazy iterator: one native // `next` per consumer step, `destroy` on exhaustion or early exit. // Buffered elements arrive as encoded buffers decoded per step. - let wrap_elem = if is_buffered(inner) { + let wrap_elem = if inner.is_buffered() { format!("(_e) => __decode({}, _e)", js_reader_fn(inner)) } else { "null".to_string() @@ -237,7 +236,7 @@ fn emit_wrapper_body_js( } if let Some(ret) = f.ret.as_ref() { - if is_buffered(ret) { + if ret.is_buffered() { let reader = js_reader_fn(ret); if f.is_async { w.line(format!( @@ -404,7 +403,7 @@ fn render_listener_wrapper_js( let Some(cb) = m.callback(&l.event_callback) else { return; }; - if !cb.params.iter().any(|p| is_buffered(&p.ty)) { + if !cb.params.iter().any(|p| p.ty.is_buffered()) { return; } let register = js_fn_name(&m.path, &format!("register_{}", l.name), strip); @@ -414,7 +413,7 @@ fn render_listener_wrapper_js( .iter() .map(|p| { let n = js_param_name(&p.name); - if is_buffered(&p.ty) { + if p.ty.is_buffered() { format!("__decode({}, {n})", js_reader_fn(&p.ty)) } else { n diff --git a/crates/weaveffi-gen-node/src/lib.rs b/crates/weaveffi-gen-node/src/lib.rs index 86651158..114fb34e 100644 --- a/crates/weaveffi-gen-node/src/lib.rs +++ b/crates/weaveffi-gen-node/src/lib.rs @@ -30,9 +30,6 @@ mod package; mod runtime; mod types; -#[cfg(test)] -mod tests; - use camino::Utf8Path; use serde::{Deserialize, Serialize}; use weaveffi_core::backend::{LanguageBackend, OutputFile}; @@ -232,5 +229,3 @@ impl LanguageBackend for NodeGenerator { Some(files) } } - -weaveffi_core::impl_generator_via_backend!(NodeGenerator); diff --git a/crates/weaveffi-gen-node/src/tests.rs b/crates/weaveffi-gen-node/src/tests.rs deleted file mode 100644 index 7f9d3f9c..00000000 --- a/crates/weaveffi-gen-node/src/tests.rs +++ /dev/null @@ -1,2462 +0,0 @@ -//! Unit tests: golden-content assertions over every emitted file. - -use super::*; -use crate::addon::render_addon_c; -use crate::entities::render_node_index; -use crate::package::render_node_dts; -use crate::types::ts_type_for; -use weaveffi_core::codegen::Generator; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_ir::ir::{ - Api, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, InterfaceDef, Module, Param, - StructDef, StructField, TypeRef, -}; - -#[test] -fn package_uses_optional_dependencies_per_platform() { - use camino::Utf8Path; - use weaveffi_core::package::{FileContent, PackageContext}; - use weaveffi_core::platform::{BinarySet, Platform}; - - let api = make_api(vec![make_module("calc")]); - let model = BindingModel::build(&api, "weaveffi"); - let mut bins = BinarySet::new("calculator"); - bins.insert(Platform::MacosArm64, "/s/darwin-arm64/libcalculator.dylib"); - bins.insert(Platform::WindowsX64, "/s/windows-x64/calculator.dll"); - let ctx = PackageContext { - binaries: &bins, - input_basename: Some("calculator.yml"), - }; - let files = LanguageBackend::package( - &NodeGenerator, - &api, - &model, - &ctx, - Utf8Path::new("/out"), - &NodeConfig::default(), - ) - .expect("node supports packaging"); - - assert_eq!(files.iter().filter(|f| f.is_binary()).count(), 2); - let main = files - .iter() - .find(|f| f.path.as_str().ends_with("node/package.json")) - .expect("main package.json present"); - let FileContent::Text(pkg) = &main.content else { - panic!("package.json is text"); - }; - assert!(pkg.contains("\"optionalDependencies\"")); - assert!(pkg.contains("weaveffi-darwin-arm64") && pkg.contains("weaveffi-win32-x64")); - // The per-platform native package is gated by npm os/cpu. - let plat = files - .iter() - .find(|f| { - f.path - .as_str() - .ends_with("npm/weaveffi-win32-x64/package.json") - }) - .expect("platform package present"); - let FileContent::Text(pp) = &plat.content else { - panic!("platform package.json is text"); - }; - assert!( - pp.contains("\"os\": [\"win32\"]") && pp.contains("\"cpu\": [\"x64\"]"), - "os/cpu gating missing: {pp}" - ); -} - -fn make_api(modules: Vec) -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules, - generators: None, - package: None, - }) -} - -fn make_module(name: &str) -> Module { - Module { - name: name.into(), - functions: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - } -} - -fn field(name: &str, ty: TypeRef) -> StructField { - StructField { - name: name.into(), - ty, - doc: None, - } -} - -fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - mutable: false, - doc: None, - } -} - -fn func(name: &str, params: Vec, returns: Option, throws: bool) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - r#async: false, - cancellable: false, - throws, - deprecated: None, - since: None, - } -} - -/// A `Contact { name: string, age: i32 }` record for buffered-type tests. -fn contact_struct() -> StructDef { - StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - field("name", TypeRef::StringUtf8), - field("age", TypeRef::I32), - ], - } -} - -/// Test-only bridge from an inline [`Api`] literal to the model the -/// production path receives from the driver. -fn build_model(api: &ResolvedApi) -> BindingModel { - BindingModel::build(api, "weaveffi") -} - -fn index_for(api: &ResolvedApi, strip: bool) -> String { - render_node_index(&build_model(api), strip, "weaveffi.yml") -} - -fn dts_for(api: &ResolvedApi, strip: bool) -> String { - render_node_dts(&build_model(api), strip, "weaveffi.yml") -} - -fn addon_for(api: &ResolvedApi, strip: bool) -> String { - render_addon_c(&build_model(api), strip, "weaveffi.yml") -} - -#[test] -fn listeners_generate_tsfn_register_unregister() { - use weaveffi_ir::ir::{CallbackDef, ListenerDef}; - let api = make_api(vec![Module { - name: "events".into(), - functions: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![CallbackDef { - name: "OnMessage".into(), - doc: None, - params: vec![param("message", TypeRef::StringUtf8)], - }], - listeners: vec![ListenerDef { - name: "message_listener".into(), - event_callback: "OnMessage".into(), - doc: None, - }], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - let dir = tempfile::tempdir().unwrap(); - let out = Utf8Path::from_path(dir.path()).unwrap(); - NodeGenerator - .generate(&api, out, &NodeConfig::default()) - .unwrap(); - let addon = std::fs::read_to_string(dir.path().join("node/weaveffi_addon.c")).unwrap(); - assert!( - addon.contains("napi_create_threadsafe_function"), - "listeners must use threadsafe functions: {addon}" - ); - assert!( - addon.contains("Napi_weaveffi_events_register_message_listener"), - "register N-API fn missing: {addon}" - ); - assert!( - addon.contains("Napi_weaveffi_events_unregister_message_listener"), - "unregister N-API fn missing: {addon}" - ); - assert!( - addon.contains("napi_call_threadsafe_function(ctx->tsfn, p, napi_tsfn_nonblocking)"), - "trampoline must queue payloads: {addon}" - ); - assert!( - addon.contains("napi_unref_threadsafe_function"), - "tsfn must be unref'd so listeners don't pin the loop: {addon}" - ); - let dts = std::fs::read_to_string(dir.path().join("node/types.d.ts")).unwrap(); - assert!( - dts.contains( - "export function registerMessageListener(callback: (message: string) => void): number" - ), - "register dts missing: {dts}" - ); - assert!( - dts.contains("export function unregisterMessageListener(id: number): void"), - "unregister dts missing: {dts}" - ); -} - -#[test] -fn ts_type_for_primitives() { - assert_eq!(ts_type_for(&TypeRef::I32), "number"); - assert_eq!(ts_type_for(&TypeRef::Bool), "boolean"); - assert_eq!(ts_type_for(&TypeRef::StringUtf8), "string"); - assert_eq!(ts_type_for(&TypeRef::Bytes), "Buffer"); - assert_eq!(ts_type_for(&TypeRef::Handle), "bigint"); -} - -#[test] -fn ts_type_for_struct_and_enum() { - assert_eq!(ts_type_for(&TypeRef::Record("Contact".into())), "Contact"); - assert_eq!(ts_type_for(&TypeRef::Enum("Color".into())), "Color"); - assert_eq!( - ts_type_for(&TypeRef::TypedHandle("Contact".into())), - "Contact" - ); -} - -#[test] -fn ts_type_for_cross_module_uses_local_name() { - // A typed handle resolved to a parent-module struct (`kv.Store`) must - // emit the bare local interface name, the only TS type in this module. - assert_eq!( - ts_type_for(&TypeRef::TypedHandle("kv.Store".into())), - "Store" - ); - assert_eq!(ts_type_for(&TypeRef::Record("kv.Store".into())), "Store"); - assert_eq!(ts_type_for(&TypeRef::Enum("kv.Kind".into())), "Kind"); -} - -#[test] -fn ts_type_for_optional() { - let ty = TypeRef::Optional(Box::new(TypeRef::StringUtf8)); - assert_eq!(ts_type_for(&ty), "string | null"); -} - -#[test] -fn ts_type_for_list() { - let ty = TypeRef::List(Box::new(TypeRef::I32)); - assert_eq!(ts_type_for(&ty), "number[]"); -} - -#[test] -fn ts_type_for_list_of_optional() { - let ty = TypeRef::List(Box::new(TypeRef::Optional(Box::new(TypeRef::I32)))); - assert_eq!(ts_type_for(&ty), "(number | null)[]"); -} - -#[test] -fn ts_type_for_map() { - let ty = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)); - assert_eq!(ts_type_for(&ty), "Record"); -} - -#[test] -fn ts_type_for_optional_list() { - let ty = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::I32)))); - assert_eq!(ts_type_for(&ty), "number[] | null"); -} - -#[test] -fn generate_node_dts_with_structs() { - let mut m = make_module("contacts"); - m.structs.push(StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - field("name", TypeRef::StringUtf8), - field("age", TypeRef::I32), - field("active", TypeRef::Bool), - ], - }); - m.enums.push(EnumDef { - name: "Color".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Blue".into(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }); - m.functions.push(func( - "get_contact", - vec![param("id", TypeRef::I32)], - Some(TypeRef::Optional(Box::new(TypeRef::Record( - "Contact".into(), - )))), - false, - )); - m.functions.push(func( - "list_contacts", - vec![], - Some(TypeRef::List(Box::new(TypeRef::Record("Contact".into())))), - false, - )); - - let dts = dts_for(&make_api(vec![m]), true); - - assert!(dts.contains("export interface Contact {")); - assert!(dts.contains(" name: string;")); - assert!(dts.contains(" age: number;")); - assert!(dts.contains(" active: boolean;")); - assert!(dts.contains("export enum Color {")); - assert!(dts.contains(" Red = 0,")); - assert!(dts.contains(" Green = 1,")); - assert!(dts.contains(" Blue = 2,")); - assert!(dts.contains("export function getContact(id: number): Contact | null")); - assert!(dts.contains("export function listContacts(): Contact[]")); - - let iface_pos = dts.find("export interface Contact").unwrap(); - let enum_pos = dts.find("export enum Color").unwrap(); - let fn_pos = dts.find("export function getContact").unwrap(); - assert!( - iface_pos < fn_pos, - "interface should appear before functions" - ); - assert!(enum_pos < fn_pos, "enum should appear before functions"); -} - -#[test] -fn node_generates_binding_gyp() { - let api = make_api(vec![{ - let mut m = make_module("math"); - m.functions.push(func( - "add", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - false, - )); - m - }]); - - let tmp = std::env::temp_dir().join("weaveffi_test_node_binding_gyp"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8"); - - NodeGenerator - .generate(&api, out_dir, &NodeConfig::default()) - .unwrap(); - - let gyp = std::fs::read_to_string(tmp.join("node").join("binding.gyp")).unwrap(); - assert!( - gyp.contains("\"target_name\": \"weaveffi\""), - "missing target_name: {gyp}" - ); - assert!( - gyp.contains("weaveffi_addon.c"), - "missing source file: {gyp}" - ); - - let addon = std::fs::read_to_string(tmp.join("node").join("weaveffi_addon.c")).unwrap(); - assert!( - addon.contains("napi_value Init("), - "missing Init function: {addon}" - ); - assert!( - addon.contains("weaveffi_math_add"), - "missing C ABI call: {addon}" - ); - assert!( - addon.contains("napi_get_cb_info"), - "missing napi_get_cb_info call: {addon}" - ); - - let pkg = std::fs::read_to_string(tmp.join("node").join("package.json")).unwrap(); - assert!(pkg.contains("\"gypfile\": true"), "missing gypfile: {pkg}"); - assert!( - pkg.contains("node-gyp rebuild"), - "missing install script: {pkg}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn generate_node_dts_with_structs_and_enums() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![ - func( - "get_contact", - vec![param("id", TypeRef::I32)], - Some(TypeRef::Optional(Box::new(TypeRef::Record( - "Contact".into(), - )))), - false, - ), - func( - "list_contacts", - vec![], - Some(TypeRef::List(Box::new(TypeRef::Record("Contact".into())))), - false, - ), - func( - "set_favorite_color", - vec![ - param("contact_id", TypeRef::I32), - param( - "color", - TypeRef::Optional(Box::new(TypeRef::Enum("Color".into()))), - ), - ], - None, - false, - ), - func( - "get_tags", - vec![param("contact_id", TypeRef::I32)], - Some(TypeRef::List(Box::new(TypeRef::StringUtf8))), - false, - ), - ], - structs: vec![StructDef { - name: "Contact".to_string(), - doc: None, - fields: vec![ - field("name", TypeRef::StringUtf8), - field("email", TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - field("tags", TypeRef::List(Box::new(TypeRef::StringUtf8))), - ], - }], - enums: vec![EnumDef { - name: "Color".to_string(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".to_string(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".to_string(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Blue".to_string(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - let tmp = std::env::temp_dir().join("weaveffi_test_node_structs_and_enums"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8"); - - NodeGenerator - .generate( - &api, - out_dir, - &NodeConfig { - strip_module_prefix: true, - ..NodeConfig::default() - }, - ) - .unwrap(); - - let dts = std::fs::read_to_string(tmp.join("node").join("types.d.ts")).unwrap(); - - assert!( - dts.contains("export interface Contact {"), - "missing Contact interface: {dts}" - ); - assert!(dts.contains(" name: string;"), "missing name field: {dts}"); - assert!( - dts.contains(" email: string | null;"), - "missing optional email field: {dts}" - ); - assert!( - dts.contains(" tags: string[];"), - "missing list tags field: {dts}" - ); - - assert!( - dts.contains("export enum Color {"), - "missing Color enum: {dts}" - ); - assert!(dts.contains(" Red = 0,"), "missing Red variant: {dts}"); - assert!(dts.contains(" Green = 1,"), "missing Green variant: {dts}"); - assert!(dts.contains(" Blue = 2,"), "missing Blue variant: {dts}"); - - assert!( - dts.contains("export function getContact(id: number): Contact | null"), - "missing getContact with optional return: {dts}" - ); - assert!( - dts.contains("export function listContacts(): Contact[]"), - "missing listContacts with list return: {dts}" - ); - assert!( - dts.contains( - "export function setFavoriteColor(contactId: number, color: Color | null): void" - ), - "missing setFavoriteColor with optional enum param: {dts}" - ); - assert!( - dts.contains("export function getTags(contactId: number): string[]"), - "missing getTags with list return: {dts}" - ); - - let iface_pos = dts.find("export interface Contact").unwrap(); - let enum_pos = dts.find("export enum Color").unwrap(); - let fn_pos = dts.find("export function getContact").unwrap(); - assert!( - iface_pos < fn_pos, - "interface should appear before functions" - ); - assert!(enum_pos < fn_pos, "enum should appear before functions"); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn node_custom_package_name() { - let api = make_api(vec![make_module("math")]); - - let tmp = std::env::temp_dir().join("weaveffi_test_node_custom_pkg"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8"); - - let config = NodeConfig { - package_name: Some("@myorg/cool-lib".into()), - ..NodeConfig::default() - }; - NodeGenerator.generate(&api, out_dir, &config).unwrap(); - - let pkg = std::fs::read_to_string(tmp.join("node").join("package.json")).unwrap(); - assert!( - pkg.contains("\"name\": \"@myorg/cool-lib\""), - "package.json should use custom name: {pkg}" - ); - assert!( - !pkg.contains("\"name\": \"weaveffi\""), - "package.json should not contain default name: {pkg}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn node_dts_has_jsdoc() { - let api = make_api(vec![{ - let mut m = make_module("math"); - m.functions.push(func( - "add", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - false, - )); - m.functions.push(func( - "subtract", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - false, - )); - m - }]); - - let dts = dts_for(&api, true); - - assert!( - dts.contains("Maps to C function: weaveffi_math_add"), - "missing JSDoc for add: {dts}" - ); - assert!( - dts.contains("Maps to C function: weaveffi_math_subtract"), - "missing JSDoc for subtract: {dts}" - ); -} - -#[test] -fn node_addon_has_no_todo() { - let api = make_api(vec![{ - let mut m = make_module("math"); - m.functions.push(func( - "add", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - false, - )); - m - }]); - let addon = addon_for(&api, true); - assert!( - !addon.contains("// TODO: implement"), - "generated addon.c should not contain TODO comments: {addon}" - ); -} - -#[test] -fn node_addon_extracts_args() { - let api = make_api(vec![{ - let mut m = make_module("math"); - m.functions.push(func( - "add", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - false, - )); - m - }]); - let addon = addon_for(&api, true); - assert!( - addon.contains("napi_get_cb_info"), - "generated addon.c should call napi_get_cb_info: {addon}" - ); -} - -#[test] -fn node_addon_frees_strings() { - let api = make_api(vec![{ - let mut m = make_module("greet"); - m.functions.push(func( - "hello", - vec![param("name", TypeRef::StringUtf8)], - Some(TypeRef::StringUtf8), - false, - )); - m - }]); - let addon = addon_for(&api, true); - assert!( - addon.contains("weaveffi_free_string(result)"), - "generated addon should free returned strings: {addon}" - ); - assert!( - addon.contains("#include "), - "generated addon should include string.h: {addon}" - ); - assert!( - addon.contains("#include "), - "generated addon should include stdlib.h: {addon}" - ); - assert!( - addon.contains("weaveffi_error_clear(&err)"), - "generated addon should clear errors: {addon}" - ); -} - -#[test] -fn node_custom_prefix_threads_to_user_symbols() { - let api = make_api(vec![{ - let mut m = make_module("greet"); - m.functions.push(func( - "hello", - vec![param("name", TypeRef::StringUtf8)], - Some(TypeRef::StringUtf8), - false, - )); - m - }]); - - let config = NodeConfig { - prefix: Some("myffi".into()), - ..NodeConfig::default() - }; - - let tmp = std::env::temp_dir().join("weaveffi_test_node_custom_prefix"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - NodeGenerator.generate(&api, out_dir, &config).unwrap(); - - // The output file name is a fixed library artifact name, not the ABI - // prefix, so it stays `weaveffi_addon.c` regardless of `prefix`. - let addon = std::fs::read_to_string(tmp.join("node/weaveffi_addon.c")).unwrap(); - - // User symbols pick up the configured ABI prefix. - assert!( - addon.contains("myffi_greet_hello"), - "addon should call the prefixed user symbol myffi_greet_hello: {addon}" - ); - assert!( - !addon.contains("weaveffi_greet_hello"), - "addon must not emit the hard-coded weaveffi_ user symbol: {addon}" - ); - assert!( - addon.contains("#include \"myffi.h\""), - "addon should include the prefixed header myffi.h: {addon}" - ); - - // Runtime ABI helpers are supplied by weaveffi-abi and stay literal. - assert!( - addon.contains("weaveffi_error"), - "runtime weaveffi_error must remain literal: {addon}" - ); - assert!( - addon.contains("weaveffi_free_string"), - "runtime weaveffi_free_string must remain literal: {addon}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn node_addon_checks_error() { - let api = make_api(vec![{ - let mut m = make_module("math"); - m.functions.push(func( - "add", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - false, - )); - m - }]); - let addon = addon_for(&api, true); - assert!( - addon.contains("err.code"), - "generated addon.c should check err.code: {addon}" - ); -} - -#[test] -fn node_strip_module_prefix() { - let api = make_api(vec![{ - let mut m = make_module("contacts"); - m.functions.push(func( - "create_contact", - vec![param("name", TypeRef::StringUtf8)], - Some(TypeRef::I32), - false, - )); - m - }]); - - let config = NodeConfig { - strip_module_prefix: true, - ..NodeConfig::default() - }; - - let tmp = std::env::temp_dir().join("weaveffi_test_node_strip_prefix"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - NodeGenerator.generate(&api, out_dir, &config).unwrap(); - - let dts = std::fs::read_to_string(tmp.join("node/types.d.ts")).unwrap(); - assert!( - dts.contains("export function createContact("), - "stripped name should be createContact: {dts}" - ); - assert!( - !dts.contains("export function contactsCreateContact("), - "should not contain module-prefixed name: {dts}" - ); - - let addon = std::fs::read_to_string(tmp.join("node/weaveffi_addon.c")).unwrap(); - assert!( - addon.contains("\"createContact\""), - "JS export name should be stripped: {addon}" - ); - assert!( - addon.contains("weaveffi_contacts_create_contact"), - "C ABI call should still use full name: {addon}" - ); - - // Stripping is the default; `strip_module_prefix: false` restores - // module-prefixed (still lowerCamelCase) names. - let default_cfg = NodeConfig::default(); - assert!( - default_cfg.strip_module_prefix, - "stripping must be the default" - ); - let no_strip = NodeConfig { - strip_module_prefix: false, - ..NodeConfig::default() - }; - let tmp2 = std::env::temp_dir().join("weaveffi_test_node_no_strip_prefix"); - let _ = std::fs::remove_dir_all(&tmp2); - std::fs::create_dir_all(&tmp2).unwrap(); - let out_dir2 = Utf8Path::from_path(&tmp2).expect("valid UTF-8"); - - NodeGenerator.generate(&api, out_dir2, &no_strip).unwrap(); - - let dts2 = std::fs::read_to_string(tmp2.join("node/types.d.ts")).unwrap(); - assert!( - dts2.contains("export function contactsCreateContact("), - "opting out should restore module-prefixed names: {dts2}" - ); - - let _ = std::fs::remove_dir_all(&tmp); - let _ = std::fs::remove_dir_all(&tmp2); -} - -#[test] -fn node_typed_handle_type() { - let api = make_api(vec![{ - let mut m = make_module("contacts"); - m.structs.push(contact_struct()); - m.functions.push(func( - "get_info", - vec![param("contact", TypeRef::TypedHandle("Contact".into()))], - None, - false, - )); - m - }]); - let dts = dts_for(&api, true); - assert!( - dts.contains("contact: Contact"), - "TypedHandle should use class type not bigint: {dts}" - ); -} - -#[test] -fn node_deeply_nested_optional() { - let api = make_api(vec![{ - let mut m = make_module("edge"); - m.structs.push(contact_struct()); - m.functions.push(func( - "process", - vec![param( - "data", - TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional( - Box::new(TypeRef::Record("Contact".into())), - ))))), - )], - None, - false, - )); - m - }]); - let dts = dts_for(&api, true); - assert!( - dts.contains("(Contact | null)[] | null"), - "should contain deeply nested optional type: {dts}" - ); -} - -#[test] -fn node_map_of_lists() { - let api = make_api(vec![{ - let mut m = make_module("edge"); - m.functions.push(func( - "process", - vec![param( - "scores", - TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::List(Box::new(TypeRef::I32))), - ), - )], - None, - false, - )); - m - }]); - let dts = dts_for(&api, true); - assert!( - dts.contains("Record"), - "should contain map of lists type: {dts}" - ); -} - -#[test] -fn node_enum_keyed_map() { - let api = make_api(vec![{ - let mut m = make_module("edge"); - m.structs.push(contact_struct()); - m.enums.push(EnumDef { - name: "Color".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }); - m.functions.push(func( - "process", - vec![param( - "contacts", - TypeRef::Map( - Box::new(TypeRef::Enum("Color".into())), - Box::new(TypeRef::Record("Contact".into())), - ), - )], - None, - false, - )); - m - }]); - let dts = dts_for(&api, true); - assert!( - dts.contains("Record"), - "should contain enum-keyed map type: {dts}" - ); - // The wrapper packs the map with an enum key writer (JS object keys - // arrive as strings, so the key coerces through Number) and the - // record's pack function per value. - let index = index_for(&api, true); - assert!( - index.contains( - "__encode((w, v) => __wMap(w, v, (w, k) => w.i32(Number(k)), __packContact), contacts)" - ), - "map param must pack keys and values: {index}" - ); -} - -#[test] -fn node_no_double_free_on_error() { - let api = make_api(vec![{ - let mut m = make_module("contacts"); - m.structs.push(contact_struct()); - m.functions.push(func( - "find_contact", - vec![param("name", TypeRef::StringUtf8)], - Some(TypeRef::Record("Contact".into())), - false, - )); - m - }]); - let addon = addon_for(&api, true); - assert!( - addon.contains("free(name)"), - "malloc'd JS string copy should be freed after the C call: {addon}" - ); - assert!( - !addon.contains("weaveffi_free_string(name)"), - "input string param must not use weaveffi_free_string: {addon}" - ); - let free_pos = addon - .find("free(name)") - .expect("free(name) should be present"); - let err_pos = addon - .find("if (err.code != 0)") - .expect("err.code check should be present"); - assert!( - free_pos < err_pos, - "cleanup should run before error check: free at {free_pos}, err at {err_pos}" - ); - let err_block_start = addon - .find(" if (err.code != 0) {\n") - .expect("error if block should be present"); - let after_err = &addon[err_block_start..]; - let err_block_end_rel = after_err - .find(" }\n napi_value ret;") - .expect("napi_value ret should follow error block"); - let err_block = &addon[err_block_start..err_block_start + err_block_end_rel]; - assert!( - !err_block.contains("result"), - "error path should not touch result before return NULL: {err_block}" - ); - // The buffered record return is copied into a JS Buffer, then the - // native encoding is released exactly once. - assert!( - addon.contains("weaveffi_free_bytes((uint8_t*)result, out_len);"), - "buffered return must be freed after copying: {addon}" - ); -} - -#[test] -fn node_null_check_on_optional_interface_return() { - // `Interface?` is the one optional that stays a nullable pointer at - // the ABI (every other optional is buffered), so the addon must - // null-check before surfacing the handle. - let api = make_api(vec![{ - let mut m = make_module("kv"); - m.interfaces.push(InterfaceDef { - name: "Store".into(), - doc: None, - constructors: vec![func("new", vec![], None, false)], - methods: vec![], - statics: vec![], - }); - m.functions.push(func( - "maybe_open", - vec![param("path", TypeRef::StringUtf8)], - Some(TypeRef::Optional(Box::new(TypeRef::Interface( - "Store".into(), - )))), - false, - )); - m - }]); - let addon = addon_for(&api, true); - assert!( - addon.contains("if (result == NULL)"), - "optional interface return should null-check before wrapping: {addon}" - ); - assert!( - addon.contains("napi_get_null"), - "optional absent should return JS null via napi_get_null: {addon}" - ); - let index = index_for(&api, true); - assert!( - index.contains("return _r == null ? null : Store._fromHandle(_r);"), - "the wrapper must null-check before wrapping the handle: {index}" - ); -} - -#[test] -fn node_async_returns_promise() { - let api = make_api(vec![{ - let mut m = make_module("tasks"); - m.functions.push(Function { - name: "run".into(), - params: vec![param("id", TypeRef::I32)], - returns: Some(TypeRef::StringUtf8), - doc: None, - r#async: true, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }); - m.functions.push(Function { - name: "fire_and_forget".into(), - params: vec![], - returns: None, - doc: None, - r#async: true, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }); - m - }]); - let dts = dts_for(&api, true); - assert!( - dts.contains("Promise<"), - "async function should return Promise in .d.ts: {dts}" - ); - assert!( - dts.contains("): Promise"), - "async string return should be Promise: {dts}" - ); - assert!( - dts.contains("): Promise"), - "async void return should be Promise: {dts}" - ); -} - -#[test] -fn node_addon_creates_promise() { - let api = make_api(vec![{ - let mut m = make_module("tasks"); - m.functions.push(Function { - name: "run".into(), - params: vec![param("id", TypeRef::I32)], - returns: Some(TypeRef::I32), - doc: None, - r#async: true, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }); - m - }]); - let addon = addon_for(&api, true); - assert!( - addon.contains("napi_create_promise"), - "async addon should call napi_create_promise: {addon}" - ); - assert!( - addon.contains("napi_resolve_deferred"), - "async callback should call napi_resolve_deferred: {addon}" - ); - assert!( - addon.contains("napi_reject_deferred"), - "async callback should call napi_reject_deferred: {addon}" - ); - assert!( - addon.contains("weaveffi_tasks_run_napi_actx"), - "async addon should define per-fn async context struct: {addon}" - ); - assert!( - addon.contains("weaveffi_tasks_run_async("), - "async addon should call the _async C function: {addon}" - ); - assert!( - addon.contains("weaveffi_tasks_run_napi_cb"), - "async addon should define the callback: {addon}" - ); - // The completion callback may fire on any producer thread, so it must - // queue through a threadsafe function instead of touching napi_env. - assert!( - addon.contains("napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking)"), - "completion callback must hop to the JS thread via tsfn: {addon}" - ); - assert!( - !addon.contains("napi_resolve_deferred(ctx->env"), - "deferred must never be settled from the producer thread: {addon}" - ); - // A rejection carries the copied structured payload. - assert!( - addon.contains("ctx->err_payload = (uint8_t*)malloc(err->payload_len)"), - "the error payload must be copied inside the callback: {addon}" - ); - assert!( - addon.contains( - "weaveffi_napi_error_value(env, ctx->err_code, ctx->err_msg, ctx->err_payload, ctx->err_payload_len)" - ), - "the rejection must carry the copied payload: {addon}" - ); -} - -/// The N-API deferred is created with `napi_create_promise` and settled -/// (on the JS thread) by exactly one of `napi_resolve_deferred` / -/// `napi_reject_deferred`. The per-fn async context that carries the -/// deferred + threadsafe function across threads must be allocated once -/// and freed exactly once, and the tsfn released exactly once. -#[test] -fn node_async_pins_callback_for_lifetime() { - let api = make_api(vec![{ - let mut m = make_module("tasks"); - m.functions.push(Function { - name: "run".into(), - params: vec![param("id", TypeRef::I32)], - returns: Some(TypeRef::I32), - doc: None, - r#async: true, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }); - m - }]); - let addon = addon_for(&api, true); - let create_count = addon.matches("napi_create_promise").count(); - let resolve_count = addon.matches("napi_resolve_deferred").count(); - let reject_count = addon.matches("napi_reject_deferred").count(); - let alloc_count = addon - .matches("calloc(1, sizeof(weaveffi_tasks_run_napi_actx))") - .count(); - let free_count = addon.matches("free(ctx);").count(); - let release_count = addon - .matches("napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release);") - .count(); - assert_eq!( - create_count, 1, - "expected one napi_create_promise per async fn, got {create_count}: {addon}" - ); - assert_eq!( - resolve_count, 1, - "expected one napi_resolve_deferred per async fn, got {resolve_count}: {addon}" - ); - assert_eq!( - reject_count, 1, - "expected one napi_reject_deferred per async fn, got {reject_count}: {addon}" - ); - assert_eq!( - alloc_count, free_count, - "ctx alloc / free must balance per async fn: alloc={alloc_count} free={free_count}: {addon}" - ); - assert_eq!( - release_count, 1, - "tsfn must be released exactly once per async fn, got {release_count}: {addon}" - ); -} - -fn doc_module() -> Module { - Module { - name: "docs".into(), - functions: vec![Function { - name: "do_thing".into(), - params: vec![Param { - name: "x".into(), - ty: TypeRef::I32, - mutable: false, - doc: Some("the input value".into()), - }], - returns: Some(TypeRef::I32), - doc: Some("Performs a thing.".into()), - r#async: false, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Item".into(), - doc: Some("An item we track.".into()), - fields: vec![StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: Some("Stable id".into()), - }], - }], - enums: vec![EnumDef { - name: "Kind".into(), - doc: Some("Kind of item.".into()), - variants: vec![EnumVariant { - name: "Small".into(), - value: 0, - doc: Some("A small one".into()), - fields: vec![], - }], - }], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - } -} - -#[test] -fn node_emits_doc_on_function() { - let dts = dts_for(&make_api(vec![doc_module()]), true); - assert!(dts.contains("Performs a thing."), "{dts}"); -} - -#[test] -fn node_emits_doc_on_struct() { - let dts = dts_for(&make_api(vec![doc_module()]), true); - assert!(dts.contains("/** An item we track. */"), "{dts}"); -} - -#[test] -fn node_emits_doc_on_enum_variant() { - let dts = dts_for(&make_api(vec![doc_module()]), true); - assert!(dts.contains("/** Kind of item. */"), "{dts}"); - assert!(dts.contains("/** A small one */"), "{dts}"); -} - -#[test] -fn node_emits_doc_on_field() { - let dts = dts_for(&make_api(vec![doc_module()]), true); - assert!(dts.contains("/** Stable id */"), "{dts}"); -} - -#[test] -fn node_emits_doc_on_param() { - let dts = dts_for(&make_api(vec![doc_module()]), true); - assert!(dts.contains("@param x the input value"), "{dts}"); -} - -// --- Value buffers ------------------------------------------------------- - -#[test] -fn buffer_runtime_emitted_only_when_needed() { - // A scalar-only model carries no buffer runtime. - let plain = make_api(vec![{ - let mut m = make_module("math"); - m.functions.push(func( - "add", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - false, - )); - m - }]); - let index = index_for(&plain, true); - assert!( - !index.contains("class __Writer"), - "scalar-only model must not embed the buffer runtime: {index}" - ); - - // Declaring a record pulls in the writer, reader, and combinators. - let buffered = make_api(vec![{ - let mut m = make_module("contacts"); - m.structs.push(contact_struct()); - m.functions.push(func( - "save", - vec![param("contact", TypeRef::Record("Contact".into()))], - None, - false, - )); - m - }]); - let index = index_for(&buffered, true); - for piece in [ - "class __Writer", - "class __Reader", - "function __wOpt", - "function __rList", - "function __wMap", - ] { - assert!( - index.contains(piece), - "buffer runtime piece `{piece}` missing: {index}" - ); - } -} - -#[test] -fn record_params_and_returns_use_value_buffers() { - let api = make_api(vec![{ - let mut m = make_module("contacts"); - m.structs.push(contact_struct()); - m.functions.push(func( - "save", - vec![param("contact", TypeRef::Record("Contact".into()))], - Some(TypeRef::Record("Contact".into())), - false, - )); - m - }]); - - // Addon: the record param crosses as the borrowed (ptr, len) pair the - // JS layer packed; the return is an owned encoding freed after the - // copy into a JS Buffer. - let addon = addon_for(&api, true); - assert!( - addon.contains("napi_get_buffer_info(env, args[0], &contact_raw, &contact_len);"), - "buffered param must read the packed Buffer: {addon}" - ); - assert!( - addon.contains( - "const uint8_t* result = weaveffi_contacts_save((const uint8_t*)contact_raw, contact_len, &out_len, &err);" - ), - "the call must pass ptr+len and thread out_len: {addon}" - ); - assert!( - addon.contains("napi_create_buffer_copy(env, out_len, result, NULL, &ret);"), - "the buffered return must be copied into a JS Buffer: {addon}" - ); - assert!( - addon.contains("weaveffi_free_bytes((uint8_t*)result, out_len);"), - "the owned encoding must be freed with weaveffi_free_bytes: {addon}" - ); - // Records have no native helpers at all. - assert!( - !addon.contains("Contact_get_") && !addon.contains("Contact_destroy"), - "records must not have native getters or destructors: {addon}" - ); - - // Loader: generated pack/unpack write fields in declaration order and - // the wrapper encodes the argument and decodes the result. - let index = index_for(&api, true); - assert!( - index.contains("function __packContact(w, v) {"), - "missing pack function: {index}" - ); - let name_write = index.find("w.str(v.name);").expect("pack writes name"); - let age_write = index.find("w.i32(v.age);").expect("pack writes age"); - assert!( - name_write < age_write, - "fields must pack in declaration order: {index}" - ); - assert!( - index.contains("function __unpackContact(r) {") - && index.contains("name: r.str(),") - && index.contains("age: r.i32(),"), - "missing unpack function: {index}" - ); - assert!( - index.contains( - "const _r = __invoke(addon.save, [__encode(__packContact, contact)], __generic);" - ), - "the wrapper must pack the record argument: {index}" - ); - assert!( - index.contains("return __decode(__unpackContact, _r);"), - "the wrapper must decode the record result: {index}" - ); -} - -#[test] -fn optional_record_return_is_buffered() { - // `Contact?` is buffered (the absence flag lives inside the buffer), - // so the addon must not null-check the pointer; the JS layer decodes - // the flag byte instead. - let api = make_api(vec![{ - let mut m = make_module("contacts"); - m.structs.push(contact_struct()); - m.functions.push(func( - "find", - vec![param("id", TypeRef::I32)], - Some(TypeRef::Optional(Box::new(TypeRef::Record( - "Contact".into(), - )))), - false, - )); - m - }]); - let addon = addon_for(&api, true); - assert!( - !addon.contains("if (result == NULL)"), - "buffered optional must not null-check the pointer: {addon}" - ); - assert!( - addon.contains("weaveffi_free_bytes((uint8_t*)result, out_len);"), - "buffered optional return must be freed: {addon}" - ); - let index = index_for(&api, true); - assert!( - index.contains("return __decode((r) => __rOpt(r, __unpackContact), _r);"), - "the wrapper must decode through the optional combinator: {index}" - ); -} - -#[test] -fn async_buffered_result_copied_then_decoded() { - let api = make_api(vec![{ - let mut m = make_module("tasks"); - m.structs.push(contact_struct()); - m.functions.push(Function { - name: "fetch_contact".into(), - params: vec![], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - r#async: true, - cancellable: false, - throws: false, - deprecated: None, - since: None, - }); - m - }]); - - // The completion callback receives a BORROWED buffer: it must copy - // the bytes before returning (the producer frees them afterwards). - let addon = addon_for(&api, true); - assert!( - addon.contains( - "static void weaveffi_tasks_fetch_contact_napi_cb(void* context, weaveffi_error* err, const uint8_t* result_ptr, size_t result_len) {" - ), - "callback must take the borrowed buffer slots: {addon}" - ); - assert!( - addon.contains( - "ctx->result = (uint8_t*)malloc(result_len); memcpy(ctx->result, result_ptr, result_len);" - ), - "callback must deep-copy the borrowed buffer: {addon}" - ); - assert!( - addon.contains("napi_create_buffer_copy(env, ctx->result_len,"), - "settle must surface the copied bytes as a JS Buffer: {addon}" - ); - - // The JS wrapper decodes the resolved buffer. - let index = index_for(&api, true); - assert!( - index.contains( - "return __invokeAsync(addon.fetchContact, [], __generic).then((_r) => __decode(__unpackContact, _r));" - ), - "the async wrapper must decode the resolved buffer: {index}" - ); -} - -#[test] -fn iterator_buffered_elements_decoded_and_freed() { - let api = make_api(vec![{ - let mut m = make_module("contacts"); - m.structs.push(contact_struct()); - m.functions.push(func( - "iter_contacts", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::Record( - "Contact".into(), - )))), - false, - )); - m - }]); - - // Addon: `_next` pulls the encoded element plus its length, copies it - // into a JS Buffer, then releases it with weaveffi_free_bytes. - let addon = addon_for(&api, true); - assert!( - addon.contains("size_t iter_item_len = 0;"), - "buffered elements need the extra length slot: {addon}" - ); - assert!( - addon.contains("&iter_item, &iter_item_len, &iter_err"), - "next must thread the element length out-param: {addon}" - ); - assert!( - addon.contains("napi_create_buffer_copy(env, iter_item_len, iter_item, NULL, &ret);"), - "the element must be copied into a JS Buffer: {addon}" - ); - assert!( - addon.contains("weaveffi_free_bytes((uint8_t*)iter_item, iter_item_len);"), - "the element encoding must be freed after copying: {addon}" - ); - - // Loader: the lazy iterator decodes each element buffer per step. - let index = index_for(&api, true); - assert!( - index.contains( - "return new WeaveFFIIterator(_it, addon.iterContacts_iterNext, addon.iterContacts_iterDestroy, __generic, (_e) => __decode(__unpackContact, _e));" - ), - "the iterator wrapper must decode each element: {index}" - ); -} - -#[test] -fn error_payload_fields_decoded_and_attached() { - let api = make_api(vec![{ - let mut m = make_module("kv"); - m.functions.push(func( - "get", - vec![param("key", TypeRef::StringUtf8)], - Some(TypeRef::StringUtf8), - true, - )); - m.errors = Some(ErrorDomain { - name: "KvError".into(), - codes: vec![ - ErrorCode { - name: "KeyNotFound".into(), - code: 1001, - message: "key not found".into(), - doc: None, - fields: vec![ - field("key", TypeRef::StringUtf8), - field("attempts", TypeRef::I32), - ], - }, - ErrorCode { - name: "StoreFull".into(), - code: 1003, - message: "store is full".into(), - doc: None, - fields: vec![], - }, - ], - }); - m - }]); - - // Addon: the native error helper attaches the raw payload buffer. - let addon = addon_for(&api, true); - assert!( - addon.contains("napi_set_named_property(env, err, \"payload\", payload_val);"), - "the error helper must attach the payload buffer: {addon}" - ); - assert!( - addon.contains( - "napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len));" - ), - "the sync throw must pass the payload slots: {addon}" - ); - - // Loader: codes with fields get a payload decoder; the factory - // attaches the decoded fields as properties on the error. - let index = index_for(&api, true); - assert!( - index.contains( - "const __kvErrorPayloads = Object.freeze({ 1001: (r) => ({ key: r.str(), attempts: r.i32() }) });" - ), - "missing the per-code payload decoders: {index}" - ); - assert!( - index.contains("function __kvErrorFrom(code, message, payload) {"), - "the factory must accept the payload buffer: {index}" - ); - assert!( - index.contains("Object.assign(_err, __decode(_decode, payload));"), - "decoded payload fields must land as error properties: {index}" - ); - - // Declarations: the payload fields surface as readonly properties on - // the code's error class. - let dts = dts_for(&api, true); - assert!( - dts.contains("export class KeyNotFoundError extends KvError {"), - "missing per-code class: {dts}" - ); - assert!( - dts.contains(" readonly key: string;") && dts.contains(" readonly attempts: number;"), - "payload fields must be declared on the class: {dts}" - ); -} - -#[test] -fn listener_buffered_params_decoded() { - use weaveffi_ir::ir::{CallbackDef, ListenerDef}; - let api = make_api(vec![Module { - name: "events".into(), - functions: vec![], - structs: vec![contact_struct()], - enums: vec![], - callbacks: vec![CallbackDef { - name: "OnContact".into(), - doc: None, - params: vec![param("contact", TypeRef::Record("Contact".into()))], - }], - listeners: vec![ListenerDef { - name: "contact_listener".into(), - event_callback: "OnContact".into(), - doc: None, - }], - errors: None, - interfaces: vec![], - modules: vec![], - }]); - - // Addon: the borrowed (ptr, len) argument is deep-copied by the - // trampoline, surfaced as a JS Buffer by the marshaller, then freed. - let addon = addon_for(&api, true); - assert!( - addon.contains("uint8_t* contact_ptr;") && addon.contains("size_t contact_len;"), - "the payload struct must own the copied buffer: {addon}" - ); - assert!( - addon.contains( - "if (contact_ptr != NULL && contact_len > 0) { p->contact_ptr = (uint8_t*)malloc(contact_len); memcpy(p->contact_ptr, contact_ptr, contact_len); }" - ), - "the trampoline must deep-copy the borrowed buffer: {addon}" - ); - assert!( - addon.contains("napi_create_buffer_copy(env, p->contact_len,"), - "the marshaller must surface the copied buffer: {addon}" - ); - assert!( - addon.contains("free(p->contact_ptr);"), - "the payload copy must be freed after the JS call: {addon}" - ); - - // Loader: the register wrapper decodes the buffer before invoking the - // user's callback. - let index = index_for(&api, true); - assert!( - index.contains("wv.registerContactListener = function (callback) {"), - "missing the register wrapper: {index}" - ); - assert!( - index.contains("callback(__decode(__unpackContact, contact));"), - "the wrapper must decode the buffered argument: {index}" - ); - - // Declarations type the callback in terms of the record. - let dts = dts_for(&api, true); - assert!( - dts.contains( - "export function registerContactListener(callback: (contact: Contact) => void): number" - ), - "register dts must type the record param: {dts}" - ); -} - -// --- Rich (algebraic) enum support ------------------------------------ - -/// A module mirroring `samples/shapes/shapes.yml`: a rich enum `Shape` -/// (unit + f64 + two-f32 + string/u8 variants), a plain enum `Channel`, and -/// the free functions that take/return the rich enum plus a numeric smoke. -fn shapes_module() -> Module { - fn variant(name: &str, value: i32, fields: Vec) -> EnumVariant { - EnumVariant { - name: name.into(), - value, - doc: None, - fields, - } - } - Module { - name: "shapes".into(), - functions: vec![ - func( - "describe", - vec![param("shape", TypeRef::RichEnum("Shape".into()))], - Some(TypeRef::StringUtf8), - false, - ), - func( - "scale", - vec![ - param("shape", TypeRef::RichEnum("Shape".into())), - param("factor", TypeRef::F64), - ], - Some(TypeRef::RichEnum("Shape".into())), - false, - ), - func( - "sum_bytes", - vec![param("values", TypeRef::List(Box::new(TypeRef::U8)))], - Some(TypeRef::U64), - false, - ), - ], - structs: vec![], - enums: vec![ - EnumDef { - name: "Shape".into(), - doc: None, - variants: vec![ - variant("Empty", 0, vec![]), - variant("Circle", 1, vec![field("radius", TypeRef::F64)]), - variant( - "Rectangle", - 2, - vec![field("width", TypeRef::F32), field("height", TypeRef::F32)], - ), - variant( - "Labeled", - 3, - vec![ - field("label", TypeRef::StringUtf8), - field("count", TypeRef::U8), - ], - ), - ], - }, - EnumDef { - name: "Channel".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }, - ], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - } -} - -#[test] -fn rich_enum_addon_marshals_value_buffers() { - let addon = addon_for(&make_api(vec![shapes_module()]), false); - - // Rich enums are value types: no tag reader, no per-variant - // constructors or getters, no destructor. - for gone in [ - "Shape_tag", - "Shape_Empty_new", - "Shape_Circle_new", - "Shape_Circle_get_radius", - "Shape_destroy", - ] { - assert!( - !addon.contains(gone), - "rich enums must have no native helper {gone}: {addon}" - ); - } - - // Free functions marshal the rich enum as a value buffer, in and out. - assert!( - addon.contains("napi_get_buffer_info(env, args[0], &shape_raw, &shape_len);"), - "describe must read the packed shape buffer: {addon}" - ); - assert!( - addon.contains("weaveffi_shapes_describe((const uint8_t*)shape_raw, shape_len, &err);"), - "describe must pass the borrowed ptr+len pair: {addon}" - ); - assert!( - addon.contains( - "const uint8_t* result = weaveffi_shapes_scale((const uint8_t*)shape_raw, shape_len, factor, &out_len, &err);" - ), - "scale must take a buffer and return an owned one: {addon}" - ); - assert!( - addon.contains("weaveffi_free_bytes((uint8_t*)result, out_len);"), - "the returned encoding must be freed: {addon}" - ); - - // A list parameter is buffered too. - assert!( - addon.contains("weaveffi_shapes_sum_bytes((const uint8_t*)values_raw, values_len, &err);"), - "list params must cross as value buffers: {addon}" - ); -} - -#[test] -fn rich_enum_index_js_packs_tagged_unions() { - let index = index_for(&make_api(vec![shapes_module()]), false); - - // Pack: the string tag selects the variant, then the i32 discriminant - // plus the variant's fields go on the wire in order. - assert!( - index.contains("function __packShape(w, v) {"), - "missing pack function: {index}" - ); - assert!( - index.contains("case 'Circle':") && index.contains("w.i32(1);"), - "circle variant must pack its discriminant: {index}" - ); - assert!( - index.contains("w.f64(v.radius);"), - "circle variant must pack its field: {index}" - ); - assert!( - index.contains("w.f32(v.width);") && index.contains("w.f32(v.height);"), - "rectangle variant must pack both f32 fields: {index}" - ); - assert!( - index.contains("w.str(v.label);") && index.contains("w.u8(v.count);"), - "labeled variant must pack string + u8: {index}" - ); - // An unknown tag is a caller bug surfaced as the generic brand. - assert!( - index.contains("throw new WeaveFFIError(-2, 'unknown Shape tag: ' + (v && v.tag));"), - "pack must reject unknown tags: {index}" - ); - - // Unpack: the i32 discriminant selects the variant; fields land next - // to the string tag. - assert!( - index.contains("function __unpackShape(r) {"), - "missing unpack function: {index}" - ); - assert!( - index.contains("case 0: return { tag: 'Empty' };"), - "unit variant must unpack to a bare tag: {index}" - ); - assert!( - index.contains("case 1: return { tag: 'Circle', radius: r.f64() };"), - "circle variant must unpack its field: {index}" - ); - assert!( - index.contains("case 3: return { tag: 'Labeled', label: r.str(), count: r.u8() };"), - "labeled variant must unpack in field order: {index}" - ); - assert!( - index.contains("default: throw new WeaveFFIError(-2, 'unknown Shape tag: ' + tag);"), - "unpack must reject unknown discriminants: {index}" - ); - - // Wrappers pack arguments and decode results; no classes, no handles. - assert!( - index.contains("wv.shapesScale = function (shape, factor) {") - && index.contains( - "const _r = __invoke(addon.shapesScale, [__encode(__packShape, shape), factor], __generic);" - ) - && index.contains("return __decode(__unpackShape, _r);"), - "scale must pack its argument and decode its result: {index}" - ); - assert!( - index.contains( - "return __invoke(addon.shapesDescribe, [__encode(__packShape, shape)], __generic);" - ), - "describe must pack its argument: {index}" - ); - assert!( - !index.contains("class Shape"), - "rich enums must not surface as classes: {index}" - ); -} - -#[test] -fn index_js_without_domains_wraps_with_generic_brand() { - // Even with no rich enums, interfaces, or error domains, every - // function gets a wrapper so a non-zero error slot (panic or - // marshalling failure) surfaces as the generic brand class. - let mut m = make_module("math"); - m.functions.push(func( - "add", - vec![param("a", TypeRef::I32)], - Some(TypeRef::I32), - false, - )); - let index = index_for(&make_api(vec![m]), false); - assert!( - index.contains("class WeaveFFIError extends Error {"), - "generic brand class missing: {index}" - ); - assert!( - index.contains("wv.mathAdd = function (a) {") - && index.contains("return __invoke(addon.mathAdd, [a], __generic);"), - "non-throwing fn must wrap through the generic brand: {index}" - ); - assert!( - index.contains("module.exports = wv;"), - "index must export the wrapper namespace: {index}" - ); -} - -#[test] -fn rich_enum_dts_emits_tagged_union() { - let dts = dts_for(&make_api(vec![shapes_module()]), false); - - // Rich enum -> a discriminated union keyed by a string tag. - assert!( - dts.contains("export type Shape ="), - "rich enum must be a union type: {dts}" - ); - assert!( - !dts.contains("export enum Shape") && !dts.contains("export class Shape"), - "rich enum must not be a plain enum or a class: {dts}" - ); - assert!(dts.contains("| { tag: 'Empty' }"), "{dts}"); - assert!(dts.contains("| { tag: 'Circle'; radius: number }"), "{dts}"); - assert!( - dts.contains("| { tag: 'Rectangle'; width: number; height: number }"), - "{dts}" - ); - assert!( - dts.contains("| { tag: 'Labeled'; label: string; count: number }"), - "{dts}" - ); - - // Plain enum still surfaces as a numeric `enum`. - assert!( - dts.contains("export enum Channel {"), - "plain enum stays an enum: {dts}" - ); - - // Free functions are typed in terms of the union; unstripped names - // keep the module prefix but are still lowerCamelCase. - assert!( - dts.contains("export function shapesDescribe(shape: Shape): string"), - "{dts}" - ); - assert!( - dts.contains("export function shapesScale(shape: Shape, factor: number): Shape"), - "{dts}" - ); -} - -// --- Interfaces and typed errors ---------------------------------------- - -/// A module mirroring the kvstore sample's shape: a `KvError` domain, a -/// `Store` interface (canonical `new` + non-throwing factory + throwing -/// and non-throwing methods + an async method + a static), and free -/// functions exercising the throws split and interface params/returns. -fn kv_module() -> Module { - Module { - name: "kv".into(), - functions: vec![ - func("ping", vec![], Some(TypeRef::Bool), false), - func( - "clone_store", - vec![param("source_store", TypeRef::Interface("Store".into()))], - Some(TypeRef::Interface("Store".into())), - true, - ), - ], - interfaces: vec![InterfaceDef { - name: "Store".into(), - doc: Some("A key-value store.".into()), - constructors: vec![ - func("new", vec![param("path", TypeRef::StringUtf8)], None, true), - func( - "open_readonly", - vec![param("path", TypeRef::StringUtf8)], - None, - false, - ), - ], - methods: vec![ - func( - "put", - vec![ - param("key", TypeRef::StringUtf8), - param("the_value", TypeRef::StringUtf8), - ], - None, - true, - ), - func("count", vec![], Some(TypeRef::I64), false), - func( - "list_keys", - vec![param( - "prefix", - TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - )], - Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - true, - ), - Function { - name: "compact".into(), - params: vec![], - returns: Some(TypeRef::I64), - doc: None, - r#async: true, - cancellable: false, - throws: true, - deprecated: None, - since: None, - }, - ], - statics: vec![func("default_capacity", vec![], Some(TypeRef::I64), false)], - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: Some(ErrorDomain { - name: "KvError".into(), - codes: vec![ - ErrorCode { - name: "KeyNotFound".into(), - code: 1001, - message: "key not found".into(), - doc: Some("The requested key does not exist.".into()), - fields: vec![], - }, - ErrorCode { - name: "StoreFull".into(), - code: 1003, - message: "store is full".into(), - doc: None, - fields: vec![], - }, - ], - }), - modules: vec![], - } -} - -#[test] -fn interface_addon_exposes_member_entry_points() { - let addon = addon_for(&make_api(vec![kv_module()]), true); - - // One native entry point per member plus the destructor, all named - // from the model's `{c_tag}_{member}` symbols. - for sym in [ - "static napi_value Napi_weaveffi_kv_Store_new(", - "static napi_value Napi_weaveffi_kv_Store_open_readonly(", - "static napi_value Napi_weaveffi_kv_Store_put(", - "static napi_value Napi_weaveffi_kv_Store_count(", - "static napi_value Napi_weaveffi_kv_Store_compact(", - "static napi_value Napi_weaveffi_kv_Store_default_capacity(", - "static napi_value Napi_weaveffi_kv_Store_destroy(", - ] { - assert!(addon.contains(sym), "missing entry point {sym}: {addon}"); - } - - // Constructors return the owned object pointer as an int64 handle. - assert!( - addon.contains("weaveffi_kv_Store* result = weaveffi_kv_Store_new(path, &err);"), - "ctor must call the C constructor: {addon}" - ); - // Methods read the wrapped pointer from args[0] and pass it as the - // leading C argument, ahead of the logical parameters. - assert!( - addon.contains( - "weaveffi_kv_Store_put((const weaveffi_kv_Store*)(intptr_t)self_raw, key, the_value, &err);" - ), - "method must pass self first: {addon}" - ); - // The async launcher symbol comes from the model (member base plus - // `_async`), with the self slot leading. - assert!( - addon.contains("weaveffi_kv_Store_compact_async((const weaveffi_kv_Store*)(intptr_t)self_raw, weaveffi_kv_Store_compact_napi_cb, ctx);"), - "async method must call the model's launcher with self: {addon}" - ); - // The destructor frees the object. - assert!( - addon.contains("weaveffi_kv_Store_destroy(self);"), - "destroy must free the object: {addon}" - ); - - // Members export under stripped, interface-scoped JS names. - for js in [ - "\"Store_new\"", - "\"Store_open_readonly\"", - "\"Store_put\"", - "\"Store_default_capacity\"", - "\"Store_destroy\"", - ] { - assert!(addon.contains(js), "missing JS export {js}: {addon}"); - } - - // Every failure path throws the code-and-payload-carrying error object. - assert!( - addon.contains( - "napi_throw(env, weaveffi_napi_error_value(env, err.code, err.message, err.payload_ptr, err.payload_len));" - ), - "sync errors must carry the ABI code and payload: {addon}" - ); - assert!( - addon.contains("napi_set_named_property(env, err, \"code\", code_val);"), - "the error helper must attach the numeric code: {addon}" - ); -} - -#[test] -fn iterator_addon_is_lazy() { - let addon = addon_for(&make_api(vec![kv_module()]), true); - - // The launch entry point never drains: it boxes the owned handle - // into a state cell and wraps it in an external with a finalizer. - assert!( - !addon.contains("while (weaveffi_kv_Store_ListKeysIterator_next"), - "the addon must not drain the iterator into an array: {addon}" - ); - assert!( - addon.contains( - "weaveffi_napi_iter_state* iter_state = (weaveffi_napi_iter_state*)calloc(1, sizeof(weaveffi_napi_iter_state));" - ), - "launch must box the handle into a state cell: {addon}" - ); - assert!( - addon.contains( - "napi_create_external(env, iter_state, weaveffi_kv_Store_list_keys_napi_iter_finalize, NULL, &ret);" - ), - "launch must wrap the cell in an external with a finalizer: {addon}" - ); - - // Per-iterator `next` and `destroy` entry points hang off the model's - // iterator-tag symbols and export under the wrapper's addon name. - assert!( - addon.contains( - "static napi_value Napi_weaveffi_kv_Store_ListKeysIterator_next(napi_env env, napi_callback_info info) {" - ), - "missing the per-iterator next entry point: {addon}" - ); - assert!( - addon.contains( - "static napi_value Napi_weaveffi_kv_Store_ListKeysIterator_destroy(napi_env env, napi_callback_info info) {" - ), - "missing the per-iterator destroy entry point: {addon}" - ); - assert!( - addon.contains("\"Store_list_keys_iterNext\"") - && addon.contains("\"Store_list_keys_iterDestroy\""), - "next/destroy must export under the wrapper's addon names: {addon}" - ); - - // One producer pull per call, threading the per-step error slot. - assert!( - addon.contains( - "if (!weaveffi_kv_Store_ListKeysIterator_next((weaveffi_kv_Store_ListKeysIterator*)state->iter, &iter_item, &iter_err)) {" - ), - "next must issue exactly one producer pull with the error slot: {addon}" - ); - // A per-step fault throws the code-carrying error (list_keys is - // `throws`, so the JS layer maps it to the domain class). - assert!( - addon.contains( - "napi_throw(env, weaveffi_napi_error_value(env, iter_err.code, iter_err.message, iter_err.payload_ptr, iter_err.payload_len));" - ), - "next must throw the per-step error: {addon}" - ); - // Each yielded string element is freed after the JS string exists. - let convert = addon - .find("napi_create_string_utf8(env, iter_item ? iter_item : \"\", NAPI_AUTO_LENGTH, &ret);") - .expect("next must convert the yielded element"); - let free = addon - .find("weaveffi_free_string((char*)iter_item);") - .expect("next must free the yielded string"); - assert!( - convert < free, - "the element must be converted before it is freed: {addon}" - ); - - // Every destroy site nulls the cell first, so exhaustion, explicit - // destroy, and the finalizer never double-free. - assert!( - addon.contains( - "weaveffi_kv_Store_ListKeysIterator_destroy((weaveffi_kv_Store_ListKeysIterator*)state->iter);" - ), - "destroy must release through the state cell: {addon}" - ); - assert!( - addon.contains("if (state != NULL && state->iter != NULL) {"), - "explicit destroy must guard against double-destroy: {addon}" - ); - assert!( - addon.contains( - "static void weaveffi_kv_Store_list_keys_napi_iter_finalize(napi_env env, void* data, void* hint) {" - ), - "abandoned iterators must be reclaimed by a finalizer: {addon}" - ); -} - -#[test] -fn iterator_js_class_implements_protocol() { - let index = index_for(&make_api(vec![kv_module()]), true); - - // The shared class implements the iterator protocol lazily. - assert!( - index.contains("class WeaveFFIIterator {"), - "missing the shared iterator class: {index}" - ); - assert!( - index.contains("[Symbol.iterator]() {"), - "the class must be iterable: {index}" - ); - assert!( - index.contains("return(value) {"), - "the class must clean up on early exit: {index}" - ); - // One native pull per step, routed through the rebranding helper. - assert!( - index.contains("const _v = __invoke(this._nextFn, [this._ext], this._map);"), - "next() must issue one native pull: {index}" - ); - // Early exit destroys the native handle exactly once. - assert!( - index.contains("this._destroyFn(this._ext);"), - "return() must destroy the native handle: {index}" - ); - - // The method wrapper launches (packing the optional prefix into a - // value buffer), then hands the external to the class with its - // per-iterator next/destroy bindings and error mapping. - assert!( - index.contains( - "const _it = __invoke(addon.Store_list_keys, [this._handle, __encode((w, v) => __wOpt(w, v, (w, v) => w.str(v)), prefix)], __kvErrorFrom);" - ), - "the wrapper must pack the optional param and launch: {index}" - ); - assert!( - index.contains( - "return new WeaveFFIIterator(_it, addon.Store_list_keys_iterNext, addon.Store_list_keys_iterDestroy, __kvErrorFrom, null);" - ), - "the wrapper must return the lazy iterator: {index}" - ); -} - -#[test] -fn iterator_dts_is_iterable_iterator() { - let dts = dts_for(&make_api(vec![kv_module()]), true); - assert!( - dts.contains("IterableIterator"), - "iter must surface as IterableIterator: {dts}" - ); - assert!( - !dts.contains("string[]"), - "iter must not surface as an array: {dts}" - ); -} - -#[test] -fn interface_index_js_class() { - let index = index_for(&make_api(vec![kv_module()]), true); - - assert!( - index.contains("class Store {"), - "missing Store class: {index}" - ); - // The canonical `new` constructor maps to the JS constructor and - // routes failures through the domain factory (it throws). - assert!( - index.contains("constructor(path) {") - && index.contains("this._handle = __invoke(addon.Store_new, [path], __kvErrorFrom);"), - "missing canonical constructor: {index}" - ); - // Other constructors become static factories; this one does not - // throw, so failures rebrand as the generic class. - assert!( - index.contains("static openReadonly(path) {") - && index.contains("__invoke(addon.Store_open_readonly, [path], __generic)") - && index.contains("return Store._fromHandle(_r);"), - "missing factory wrapping the owned handle: {index}" - ); - // Methods pass the wrapped handle as the leading argument. - assert!( - index.contains("put(key, theValue) {") - && index.contains( - "return __invoke(addon.Store_put, [this._handle, key, theValue], __kvErrorFrom);" - ), - "missing method with leading self handle: {index}" - ); - // The async method rejects typed (it throws). - assert!( - index.contains("compact() {") - && index.contains( - "return __invokeAsync(addon.Store_compact, [this._handle], __kvErrorFrom);" - ), - "missing async method: {index}" - ); - // Statics are static methods. - assert!( - index.contains("static defaultCapacity() {") - && index.contains("return __invoke(addon.Store_default_capacity, [], __generic);"), - "missing static method: {index}" - ); - // Disposal follows the opaque-wrapper idiom: explicit destroy plus a - // FinalizationRegistry safety net calling the destroy export. - assert!( - index.contains("destroy() {") && index.contains("addon.Store_destroy(this._handle);"), - "missing destroy(): {index}" - ); - assert!( - index.contains("Store._cleanup = new FinalizationRegistry"), - "missing FinalizationRegistry: {index}" - ); - - // A free function borrowing an interface unwraps the class argument - // and wraps the owned returned handle in a new instance. - assert!( - index.contains("wv.cloneStore = function (sourceStore) {") - && index.contains( - "__invoke(addon.cloneStore, [sourceStore instanceof Store ? sourceStore._handle : sourceStore], __kvErrorFrom)" - ) - && index.contains("return Store._fromHandle(_r);"), - "interface param/return must cross as instances: {index}" - ); -} - -#[test] -fn typed_error_classes_js() { - let index = index_for(&make_api(vec![kv_module()]), true); - - // Domain class extends the generic brand; per-code subclasses carry - // their stable CODE and default message. - assert!( - index.contains("class KvError extends WeaveFFIError {"), - "missing domain class: {index}" - ); - assert!( - index.contains("class KeyNotFoundError extends KvError {"), - "missing per-code class: {index}" - ); - assert!( - index.contains("KeyNotFoundError.CODE = 1001;") - && index.contains("StoreFullError.CODE = 1003;"), - "missing stable code constants: {index}" - ); - assert!( - index.contains("super(1001, message || 'key not found');"), - "per-code class must default its message: {index}" - ); - // The factory maps a raw code (plus the raw payload buffer) to the - // matching class and falls back to the generic brand for unknown - // codes (panics, marshalling). - assert!( - index.contains("function __kvErrorFrom(code, message, payload) {"), - "missing domain factory: {index}" - ); - assert!( - index.contains("1001: KeyNotFoundError, 1003: StoreFullError"), - "missing code table: {index}" - ); - assert!( - index.contains( - "const _err = _cls === undefined ? new WeaveFFIError(code, message) : new _cls(message);" - ), - "factory must fall back to the generic brand: {index}" - ); - // Both surfaces are exported. - assert!( - index.contains("wv.KvError = KvError;") - && index.contains("wv.KeyNotFoundError = KeyNotFoundError;"), - "error classes must be exported: {index}" - ); -} - -#[test] -fn throws_split_picks_the_error_surface() { - let index = index_for(&make_api(vec![kv_module()]), true); - - // throws == false: plain wrapper; a non-zero error slot (panic or - // marshalling failure only) still rebrands as the generic class. - assert!( - index.contains("wv.ping = function () {") - && index.contains("return __invoke(addon.ping, [], __generic);"), - "non-throwing fn must use the generic map: {index}" - ); - // throws == true: failures map through the module's domain factory. - assert!( - index.contains("__invoke(addon.cloneStore, [sourceStore instanceof Store ? sourceStore._handle : sourceStore], __kvErrorFrom)"), - "throwing fn must use the domain map: {index}" - ); -} - -#[test] -fn typed_error_and_interface_dts() { - let dts = dts_for(&make_api(vec![kv_module()]), true); - - // The generic brand plus the domain surface. - assert!( - dts.contains("export class WeaveFFIError extends Error {"), - "missing generic brand: {dts}" - ); - assert!( - dts.contains("export class KvError extends WeaveFFIError {"), - "missing domain class: {dts}" - ); - assert!( - dts.contains("export class KeyNotFoundError extends KvError {") - && dts.contains("static readonly CODE: 1001;"), - "missing per-code class: {dts}" - ); - - // The interface class mirrors the JS surface. - assert!( - dts.contains("export class Store {"), - "missing Store class: {dts}" - ); - assert!( - dts.contains("constructor(path: string);"), - "missing canonical constructor: {dts}" - ); - assert!( - dts.contains("static openReadonly(path: string): Store;"), - "missing factory: {dts}" - ); - assert!( - dts.contains("put(key: string, theValue: string): void;"), - "missing method with camel params: {dts}" - ); - assert!( - dts.contains("compact(): Promise;"), - "missing async method: {dts}" - ); - assert!( - dts.contains("static defaultCapacity(): number;"), - "missing static: {dts}" - ); - assert!(dts.contains("destroy(): void;"), "missing destroy: {dts}"); - - // Throwing callables document their domain; interface params and - // returns are typed as the class. - assert!( - dts.contains("@throws {KvError}"), - "missing @throws tag: {dts}" - ); - assert!( - dts.contains("export function cloneStore(sourceStore: Store): Store"), - "missing interface-typed free function: {dts}" - ); - assert!( - dts.contains("export function ping(): boolean"), - "missing plain function: {dts}" - ); -} diff --git a/crates/weaveffi-gen-node/src/types.rs b/crates/weaveffi-gen-node/src/types.rs index 724d5b17..daf1caa6 100644 --- a/crates/weaveffi-gen-node/src/types.rs +++ b/crates/weaveffi-gen-node/src/types.rs @@ -8,9 +8,9 @@ use heck::ToLowerCamelCase; use weaveffi_core::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle}; use weaveffi_core::lang; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ErrorBinding, FnBinding, ParamBinding}; use weaveffi_core::utils::{local_type_name, wrapper_name}; -use weaveffi_ir::ir::TypeRef; /// The exported JS name of a free function or listener endpoint: /// [`wrapper_name`] (module-prefixed or stripped per config) converted to @@ -43,47 +43,46 @@ pub(crate) fn js_str_literal(s: &str) -> String { } /// The TS annotation of one IR type. -pub(crate) fn ts_type_for(ty: &TypeRef) -> String { +pub(crate) fn ts_type_for(ty: &Ty) -> String { match ty { - TypeRef::I8 - | TypeRef::I16 - | TypeRef::U8 - | TypeRef::U16 - | TypeRef::I32 - | TypeRef::U32 - | TypeRef::I64 - | TypeRef::U64 - | TypeRef::F32 - | TypeRef::F64 => "number".into(), - TypeRef::Bool => "boolean".into(), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "string".into(), - TypeRef::Bytes | TypeRef::BorrowedBytes => "Buffer".into(), - TypeRef::Handle => "bigint".into(), + Ty::I8 + | Ty::I16 + | Ty::U8 + | Ty::U16 + | Ty::I32 + | Ty::U32 + | Ty::I64 + | Ty::U64 + | Ty::F32 + | Ty::F64 => "number".into(), + Ty::Bool => "boolean".into(), + Ty::StringUtf8 | Ty::BorrowedStr => "string".into(), + Ty::Bytes | Ty::BorrowedBytes => "Buffer".into(), + Ty::Handle => "bigint".into(), // Records, rich enums, plain enums, interfaces, and typed handles // surface as bare local TS names. A cross-module reference (e.g. // `handle` resolved to `kv.Store`) must annotate the *local* // type `Store`; the qualified IR name is not a declared TS type in // this module. - TypeRef::TypedHandle(name) => local_type_name(name).to_string(), - TypeRef::Record(name) | TypeRef::RichEnum(name) => local_type_name(name).to_string(), - TypeRef::Interface(name) => local_type_name(name).to_string(), - TypeRef::Enum(name) => local_type_name(name).to_string(), - TypeRef::Optional(inner) => format!("{} | null", ts_type_for(inner)), - TypeRef::List(inner) => { + Ty::TypedHandle(name) => local_type_name(name).to_string(), + Ty::Record(name) | Ty::RichEnum(name) => local_type_name(name).to_string(), + Ty::Interface(name) => local_type_name(name).to_string(), + Ty::Enum(name) => local_type_name(name).to_string(), + Ty::Optional(inner) => format!("{} | null", ts_type_for(inner)), + Ty::List(inner) => { let inner_ts = ts_type_for(inner); - if matches!(inner.as_ref(), TypeRef::Optional(_)) { + if matches!(inner.as_ref(), Ty::Optional(_)) { format!("({inner_ts})[]") } else { format!("{inner_ts}[]") } } - TypeRef::Map(k, v) => format!("Record<{}, {}>", ts_type_for(k), ts_type_for(v)), + Ty::Map(k, v) => format!("Record<{}, {}>", ts_type_for(k), ts_type_for(v)), // `iter` is a lazy pull stream, not a materialized array. - TypeRef::Iterator(inner) => { + Ty::Iterator(inner) => { let t = ts_type_for(inner); format!("IterableIterator<{t}>") } - TypeRef::Named(_) => unreachable!("unresolved type reference"), } } diff --git a/crates/weaveffi-gen-python/Cargo.toml b/crates/weaveffi-gen-python/Cargo.toml index 6c2deb8e..5daf30d2 100644 --- a/crates/weaveffi-gen-python/Cargo.toml +++ b/crates/weaveffi-gen-python/Cargo.toml @@ -11,12 +11,8 @@ categories = ["development-tools::ffi"] [dependencies] weaveffi-core = { path = "../weaveffi-core", version = "0.21.0" } -weaveffi-ir = { path = "../weaveffi-ir", version = "0.21.0" } camino = { workspace = true } heck = { workspace = true } serde = { workspace = true } -[dev-dependencies] -tempfile = { workspace = true } - [lints] workspace = true diff --git a/crates/weaveffi-gen-python/src/calls.rs b/crates/weaveffi-gen-python/src/calls.rs index 285a7b16..6b2a0065 100644 --- a/crates/weaveffi-gen-python/src/calls.rs +++ b/crates/weaveffi-gen-python/src/calls.rs @@ -4,20 +4,20 @@ //! Marshalling dispatch is driven by the shared plans: [`ArgPass`] decides //! how each parameter crosses, [`RetPass`] how the result comes back, and //! [`plan::elem_free`] what an iterator step owes, so this module never -//! re-derives those shapes from `TypeRef` matches. +//! re-derives those shapes from `Ty` matches. use weaveffi_core::abi; use weaveffi_core::codegen::common::pascal_case; use weaveffi_core::codegen::CodeWriter; use weaveffi_core::lang; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ CallShape, CallbackBinding, ErrorBinding, FnBinding, ListenerBinding, ModuleBinding, ParamBinding, }; +use weaveffi_core::model::{Prim, WireType}; use weaveffi_core::plan::{self, ArgPass, ElemFree, ErrorStrategy, RetPass}; use weaveffi_core::utils::local_type_name; -use weaveffi_core::wire::{self, WireType}; -use weaveffi_ir::ir::TypeRef; use crate::codec::{ py_decode_borrowed_expr, py_decode_owned_expr, py_pack_fn_name, py_read_expr, @@ -145,7 +145,7 @@ pub(crate) fn render_callable( // The `_...Iterator` helper class is module-level; a member's helper is // emitted by `render_interface` ahead of the wrapper class instead. - if let (Some(TypeRef::Iterator(inner)), CallShape::Iterator(it)) = (&f.ret, &f.shape) { + if let (Some(Ty::Iterator(inner)), CallShape::Iterator(it)) = (&f.ret, &f.shape) { if !scope.is_member() { render_iterator_class(out, &it.iter_tag, &f.name, inner, &checker); } @@ -325,7 +325,7 @@ fn py_param_conversion(p: &ParamBinding, ind: &str) -> Vec { // buffered shapes (optionals, lists, maps) inline their write // statements through a per-parameter writer (so several buffered // parameters in one call never collide). - ArgPass::Buffer { .. } => match wire::classify(&p.ty) { + ArgPass::Buffer { .. } => match p.ty.wire() { WireType::User(n) => { vec![format!("{ind}_{name}_buf = {}({name})", py_pack_fn_name(n))] } @@ -367,8 +367,8 @@ fn py_param_call_args(p: &ParamBinding) -> Vec { vec![format!("{name}._ptr if {name} is not None else None")] } ArgPass::Direct { .. } => match &p.ty { - TypeRef::Bool => vec![format!("1 if {name} else 0")], - TypeRef::Enum(_) => vec![format!("{name}.value")], + Ty::Bool => vec![format!("1 if {name} else 0")], + Ty::Enum(_) => vec![format!("{name}.value")], // Scalars, handles, and typed handles are already the raw slot // value the C signature wants. _ => vec![name], @@ -379,10 +379,10 @@ fn py_param_call_args(p: &ParamBinding) -> Vec { // ── Return helpers ── /// The interface name behind a direct or optional interface return. -fn object_interface_name(ty: &TypeRef) -> &str { +fn object_interface_name(ty: &Ty) -> &str { match ty { - TypeRef::Interface(name) => name, - TypeRef::Optional(inner) => object_interface_name(inner), + Ty::Interface(name) => name, + Ty::Optional(inner) => object_interface_name(inner), _ => unreachable!("object returns are direct or optional interfaces"), } } @@ -390,7 +390,7 @@ fn object_interface_name(ty: &TypeRef) -> &str { /// Append the statements converting `_result` (and `_out_len` when present) /// into the wrapper's return value, honoring the receive-and-release /// contract of [`RetPass`]. -fn render_return_value(out: &mut String, ty: &TypeRef, ind: &str) { +fn render_return_value(out: &mut String, ty: &Ty, ind: &str) { let mut w = CodeWriter::four_space().with_depth(ind.len() / 4); // Module and prefix only shape an object return's destroy symbol, which // Python defers to the wrapper class's `__del__`; empty context is fine. @@ -399,7 +399,7 @@ fn render_return_value(out: &mut String, ty: &TypeRef, ind: &str) { // bytes, release them with `weaveffi_free_bytes`, then decode. RetPass::Buffer => { w.line("_data = _take_buffer(_result, _out_len.value)"); - match wire::classify(ty) { + match ty.wire() { WireType::User(name) => { w.line(format!("return {}(_data)", py_unpack_fn_name(name))); } @@ -445,13 +445,13 @@ fn render_return_value(out: &mut String, ty: &TypeRef, ind: &str) { RetPass::Direct => match ty { // The `c_void_p` restype surfaces null as `None`; normalize to // the integer handle representation. - TypeRef::TypedHandle(_) => { + Ty::TypedHandle(_) => { w.line("return _result or 0"); } - TypeRef::Bool => { + Ty::Bool => { w.line("return bool(_result)"); } - TypeRef::Enum(name) => { + Ty::Enum(name) => { let name = local_type_name(name); w.line(format!("return {name}(_result)")); } @@ -468,7 +468,7 @@ fn render_return_value(out: &mut String, ty: &TypeRef, ind: &str) { // ── Async rendering ── /// `(param_name, ctypes_type)` pairs for async C callback parameters after `(context, err)`. -fn py_async_cb_trailing_fields(ret: &Option) -> Vec<(String, String)> { +fn py_async_cb_trailing_fields(ret: &Option) -> Vec<(String, String)> { match ret { None => vec![], Some(ty) => abi::callback_result_params(ty, "") @@ -493,7 +493,7 @@ fn py_async_cb_trailing_fields(ret: &Option) -> Vec<(String, String)> { /// are released with `weaveffi_free_string`, value buffers with /// `weaveffi_free_bytes`, and an owned interface pointer is adopted by its /// wrapper class. -fn append_async_success_handler(out: &mut String, ret: &Option, ind: &str) { +fn append_async_success_handler(out: &mut String, ret: &Option, ind: &str) { let Some(ty) = ret else { out.push_str(&format!("{ind}_state[\"val\"] = None\n")); return; @@ -549,13 +549,13 @@ fn append_async_success_handler(out: &mut String, ret: &Option, ind: &s } RetPass::Direct => match ty { // A typed handle's `c_void_p` slot surfaces as `int | None`. - TypeRef::TypedHandle(_) => { + Ty::TypedHandle(_) => { out.push_str(&format!("{ind}_state[\"val\"] = result or 0\n")); } - TypeRef::Bool => { + Ty::Bool => { out.push_str(&format!("{ind}_state[\"val\"] = bool(result)\n")); } - TypeRef::Enum(name) => { + Ty::Enum(name) => { let name = local_type_name(name); out.push_str(&format!("{ind}_state[\"val\"] = {name}(result)\n")); } @@ -728,7 +728,7 @@ fn render_async_ffi_call_body( /// the `next` signature carries a trailing `size_t* out_len`. Owned pointer /// elements (strings, bytes, buffered values) stay raw `c_void_p` addresses /// so they survive to be freed, per [`plan::elem_free`]. -fn py_iter_slots(inner: &TypeRef) -> (String, bool) { +fn py_iter_slots(inner: &Ty) -> (String, bool) { match plan::elem_free(inner) { ElemFree::Bytes => ("ctypes.c_void_p".into(), true), ElemFree::String => ("ctypes.c_void_p".into(), false), @@ -739,13 +739,13 @@ fn py_iter_slots(inner: &TypeRef) -> (String, bool) { /// The Python expression converting a pulled `_out_item` slot into the /// element the consumer receives, honoring the owed per-element release /// ([`plan::elem_free`]). -fn py_read_iter_item(inner: &TypeRef) -> String { +fn py_read_iter_item(inner: &Ty) -> String { match plan::elem_free(inner) { // A `ptr` + `len` element: copy the bytes, free them with // `weaveffi_free_bytes` (via `_take_buffer`), then decode any // buffered value. - ElemFree::Bytes => match wire::classify(inner) { - WireType::Bytes => "_take_buffer(_out_item.value, _out_len.value)".into(), + ElemFree::Bytes => match inner.wire() { + WireType::Prim(Prim::Bytes) => "_take_buffer(_out_item.value, _out_len.value)".into(), WireType::User(name) => format!( "{}(_take_buffer(_out_item.value, _out_len.value))", py_unpack_fn_name(name) @@ -759,15 +759,15 @@ fn py_read_iter_item(inner: &TypeRef) -> String { // slot is a raw `c_void_p`, so the address survives to be freed. ElemFree::String => "_take_string(_out_item.value)".into(), ElemFree::None => match inner { - TypeRef::Enum(name) => { + Ty::Enum(name) => { format!("{}(_out_item.value)", local_type_name(name)) } // An owned interface element is adopted by its wrapper class. - TypeRef::Interface(name) => { + Ty::Interface(name) => { format!("{}._from_ptr(_out_item.value)", local_type_name(name)) } - TypeRef::TypedHandle(_) => "_out_item.value or 0".into(), - TypeRef::Bool => "bool(_out_item.value)".into(), + Ty::TypedHandle(_) => "_out_item.value or 0".into(), + Ty::Bool => "bool(_out_item.value)".into(), _ => "_out_item.value".into(), }, } @@ -792,7 +792,7 @@ pub(crate) fn render_iterator_class( out: &mut String, iter_tag: &str, func_name: &str, - inner: &TypeRef, + inner: &Ty, checker: &str, ) { let class_name = py_iterator_class_name(func_name); @@ -919,8 +919,8 @@ fn py_cb_param_expr(p: &ParamBinding) -> String { // an owning class whose `__del__` would free them. ArgPass::Object { .. } => esc, ArgPass::Direct { .. } => match &p.ty { - TypeRef::Bool => format!("bool({esc})"), - TypeRef::Enum(name) => format!("{}({esc})", local_type_name(name)), + Ty::Bool => format!("bool({esc})"), + Ty::Enum(name) => format!("{}({esc})", local_type_name(name)), // Scalars, handles, and typed handles pass through unchanged. _ => esc, }, diff --git a/crates/weaveffi-gen-python/src/codec.rs b/crates/weaveffi-gen-python/src/codec.rs index 57b242db..6415eef4 100644 --- a/crates/weaveffi-gen-python/src/codec.rs +++ b/crates/weaveffi-gen-python/src/codec.rs @@ -7,12 +7,12 @@ //! their owned forms, records and rich enums as one user-codec shape). use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{EnumBinding, StructBinding}; +use weaveffi_core::model::{Prim, WireType}; use weaveffi_core::utils::local_type_name; -use weaveffi_core::wire::{self, WireType}; -use weaveffi_ir::ir::TypeRef; -use crate::types::py_field; +use crate::types::{py_field, py_variant}; /// `_write_{Name}`, the statement-level field writer for a record or rich /// enum. `name` may be a qualified IR reference; the emitted function uses @@ -46,23 +46,23 @@ pub(crate) fn py_unpack_fn_name(name: &str) -> String { /// Python evaluates a comprehension's `range(_r.read_len())` before its body /// and a conditional expression's test before its arms, which matches the /// wire order exactly. -pub(crate) fn py_read_expr(ty: &TypeRef, depth: usize) -> String { - match wire::classify(ty) { - WireType::Bool => "_r.read_bool()".into(), - WireType::I8 => "_r.read_i8()".into(), - WireType::I16 => "_r.read_i16()".into(), - WireType::I32 => "_r.read_i32()".into(), - WireType::I64 => "_r.read_i64()".into(), - WireType::U8 => "_r.read_u8()".into(), - WireType::U16 => "_r.read_u16()".into(), - WireType::U32 => "_r.read_u32()".into(), - WireType::U64 => "_r.read_u64()".into(), - WireType::F32 => "_r.read_f32()".into(), - WireType::F64 => "_r.read_f64()".into(), +pub(crate) fn py_read_expr(ty: &Ty, depth: usize) -> String { + match ty.wire() { + WireType::Prim(Prim::Bool) => "_r.read_bool()".into(), + WireType::Prim(Prim::I8) => "_r.read_i8()".into(), + WireType::Prim(Prim::I16) => "_r.read_i16()".into(), + WireType::Prim(Prim::I32) => "_r.read_i32()".into(), + WireType::Prim(Prim::I64) => "_r.read_i64()".into(), + WireType::Prim(Prim::U8) => "_r.read_u8()".into(), + WireType::Prim(Prim::U16) => "_r.read_u16()".into(), + WireType::Prim(Prim::U32) => "_r.read_u32()".into(), + WireType::Prim(Prim::U64) => "_r.read_u64()".into(), + WireType::Prim(Prim::F32) => "_r.read_f32()".into(), + WireType::Prim(Prim::F64) => "_r.read_f64()".into(), // Handles serialize as u64 tokens inside buffers. - WireType::Handle => "_r.read_u64()".into(), - WireType::String => "_r.read_string()".into(), - WireType::Bytes => "_r.read_bytes()".into(), + WireType::Handle(_) => "_r.read_u64()".into(), + WireType::Prim(Prim::String) => "_r.read_string()".into(), + WireType::Prim(Prim::Bytes) => "_r.read_bytes()".into(), WireType::Enum(name) => format!("{}(_r.read_i32())", local_type_name(name)), WireType::User(name) => format!("{}(_r)", py_read_fn_name(name)), WireType::Optional(inner) => format!( @@ -84,54 +84,48 @@ pub(crate) fn py_read_expr(ty: &TypeRef, depth: usize) -> String { /// Append the statements writing `expr` (one `ty` value) into the /// `_BufferWriter` named `writer`, following the value-buffer wire format. /// `depth` uniquifies loop variables when composites nest. -pub(crate) fn py_write_stmts( - w: &mut CodeWriter, - writer: &str, - expr: &str, - ty: &TypeRef, - depth: usize, -) { - match wire::classify(ty) { - WireType::Bool => { +pub(crate) fn py_write_stmts(w: &mut CodeWriter, writer: &str, expr: &str, ty: &Ty, depth: usize) { + match ty.wire() { + WireType::Prim(Prim::Bool) => { w.line(format!("{writer}.write_bool({expr})")); } - WireType::I8 => { + WireType::Prim(Prim::I8) => { w.line(format!("{writer}.write_i8({expr})")); } - WireType::I16 => { + WireType::Prim(Prim::I16) => { w.line(format!("{writer}.write_i16({expr})")); } - WireType::I32 => { + WireType::Prim(Prim::I32) => { w.line(format!("{writer}.write_i32({expr})")); } - WireType::I64 => { + WireType::Prim(Prim::I64) => { w.line(format!("{writer}.write_i64({expr})")); } - WireType::U8 => { + WireType::Prim(Prim::U8) => { w.line(format!("{writer}.write_u8({expr})")); } - WireType::U16 => { + WireType::Prim(Prim::U16) => { w.line(format!("{writer}.write_u16({expr})")); } - WireType::U32 => { + WireType::Prim(Prim::U32) => { w.line(format!("{writer}.write_u32({expr})")); } - WireType::U64 => { + WireType::Prim(Prim::U64) => { w.line(format!("{writer}.write_u64({expr})")); } - WireType::F32 => { + WireType::Prim(Prim::F32) => { w.line(format!("{writer}.write_f32({expr})")); } - WireType::F64 => { + WireType::Prim(Prim::F64) => { w.line(format!("{writer}.write_f64({expr})")); } - WireType::Handle => { + WireType::Handle(_) => { w.line(format!("{writer}.write_u64({expr})")); } - WireType::String => { + WireType::Prim(Prim::String) => { w.line(format!("{writer}.write_string({expr})")); } - WireType::Bytes => { + WireType::Prim(Prim::Bytes) => { w.line(format!("{writer}.write_bytes({expr})")); } // IntEnum members are ints, so the discriminant packs directly. @@ -173,9 +167,9 @@ pub(crate) fn py_write_stmts( /// The expression decoding a borrowed `(ptr, len)` buffer pair (a callback /// or listener argument) into its idiomatic value. The producer owns the /// buffer for the dispatch, so the bytes are copied before decoding. -pub(crate) fn py_decode_borrowed_expr(ptr: &str, len: &str, ty: &TypeRef) -> String { +pub(crate) fn py_decode_borrowed_expr(ptr: &str, len: &str, ty: &Ty) -> String { let data = format!("ctypes.string_at({ptr}, {len}) if {ptr} else b\"\""); - match wire::classify(ty) { + match ty.wire() { WireType::User(name) => format!("{}({data})", py_unpack_fn_name(name)), _ => format!("_decode_buffer({data}, lambda _r: {})", py_read_expr(ty, 0)), } @@ -185,9 +179,9 @@ pub(crate) fn py_decode_borrowed_expr(ptr: &str, len: &str, ty: &TypeRef) -> Str /// completion result) into its idiomatic value. The consumer owns the buffer, /// so `_take_buffer` copies it and releases the producer allocation with /// `weaveffi_free_bytes`. -pub(crate) fn py_decode_owned_expr(ptr: &str, len: &str, ty: &TypeRef) -> String { +pub(crate) fn py_decode_owned_expr(ptr: &str, len: &str, ty: &Ty) -> String { let data = format!("_take_buffer(ctypes.cast({ptr}, ctypes.c_void_p).value, {len})"); - match wire::classify(ty) { + match ty.wire() { WireType::User(name) => format!("{}({data})", py_unpack_fn_name(name)), _ => format!("_decode_buffer({data}, lambda _r: {})", py_read_expr(ty, 0)), } @@ -252,7 +246,7 @@ pub(crate) fn render_rich_enum_codecs(w: &mut CodeWriter, e: &EnumBinding) { )); w.scope(|w| { for v in &e.variants { - let class = format!("{name}{}", v.name); + let class = format!("{name}{}", py_variant(&v.name)); w.line(format!("if isinstance(value, {class}):")); w.scope(|w| { w.line(format!("_w.write_i32({})", v.value)); @@ -272,7 +266,7 @@ pub(crate) fn render_rich_enum_codecs(w: &mut CodeWriter, e: &EnumBinding) { w.scope(|w| { w.line("_tag = _r.read_i32()"); for v in &e.variants { - let class = format!("{name}{}", v.name); + let class = format!("{name}{}", py_variant(&v.name)); w.line(format!("if _tag == {}:", v.value)); w.scope(|w| { if v.fields.is_empty() { diff --git a/crates/weaveffi-gen-python/src/entities.rs b/crates/weaveffi-gen-python/src/entities.rs index cb6d370b..af4c0bdf 100644 --- a/crates/weaveffi-gen-python/src/entities.rs +++ b/crates/weaveffi-gen-python/src/entities.rs @@ -3,16 +3,16 @@ use heck::{ToShoutySnakeCase, ToSnakeCase}; use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ CallShape, EnumBinding, ErrorBinding, ErrorCodeBinding, FieldBinding, FnBinding, InterfaceBinding, ModuleBinding, StructBinding, }; -use weaveffi_ir::ir::TypeRef; use crate::calls::{render_callable, render_iterator_class, FnScope}; use crate::codec::{py_read_expr, render_record_codecs, render_rich_enum_codecs}; use crate::docs::emit_docstring; -use crate::types::{py_field, py_str_literal, py_type_hint}; +use crate::types::{py_field, py_str_literal, py_type_hint, py_variant}; // ── Errors ── @@ -242,7 +242,7 @@ pub(crate) fn render_enum(out: &mut String, e: &EnumBinding) { } } } - w.line(format!("{} = {}", v.name, v.value)); + w.line(format!("{} = {}", py_variant(&v.name), v.value)); } out.push_str(&w.finish()); } @@ -278,7 +278,7 @@ fn render_rich_enum(out: &mut String, e: &EnumBinding) { } } } - w.line(format!("{} = {}", v.name, v.value)); + w.line(format!("{} = {}", py_variant(&v.name), v.value)); } }); w.blank(); @@ -292,7 +292,7 @@ fn render_rich_enum(out: &mut String, e: &EnumBinding) { // One module-level dataclass per variant, subclassing the base. for v in &e.variants { - let class = format!("{name}{}", v.name); + let class = format!("{name}{}", py_variant(&v.name)); w.blank().blank(); w.line("@dataclass"); w.line(format!("class {class}({name}):")); @@ -303,7 +303,7 @@ fn render_rich_enum(out: &mut String, e: &EnumBinding) { w.raw(doc); w.blank(); } - w.line(format!("TAG = {name}.Tag.{}", v.name)); + w.line(format!("TAG = {name}.Tag.{}", py_variant(&v.name))); if !v.fields.is_empty() { w.blank(); render_dataclass_fields(&mut w, &v.fields); @@ -315,7 +315,7 @@ fn render_rich_enum(out: &mut String, e: &EnumBinding) { // exists. w.blank().blank(); for v in &e.variants { - w.line(format!("{name}.{} = {name}{}", v.name, v.name)); + w.line(format!("{name}.{0} = {name}{0}", py_variant(&v.name))); } render_rich_enum_codecs(&mut w, e); @@ -386,7 +386,7 @@ pub(crate) fn render_interface(out: &mut String, module: &ModuleBinding, i: &Int // wrapper so nothing nests inside the class body. The interface name // qualifies the helper so two interfaces can share a method name. for m in i.methods.iter().chain(i.statics.iter()) { - if let (Some(TypeRef::Iterator(inner)), CallShape::Iterator(it)) = (&m.ret, &m.shape) { + if let (Some(Ty::Iterator(inner)), CallShape::Iterator(it)) = (&m.ret, &m.shape) { let checker = py_checker_name(m, error); render_iterator_class( out, diff --git a/crates/weaveffi-gen-python/src/lib.rs b/crates/weaveffi-gen-python/src/lib.rs index 94fe2147..77675511 100644 --- a/crates/weaveffi-gen-python/src/lib.rs +++ b/crates/weaveffi-gen-python/src/lib.rs @@ -18,9 +18,6 @@ mod runtime; mod stubs; mod types; -#[cfg(test)] -mod tests; - use camino::Utf8Path; use serde::{Deserialize, Serialize}; use weaveffi_core::backend::{LanguageBackend, OutputFile}; @@ -99,9 +96,7 @@ pub struct PythonGenerator; impl PythonGenerator { /// Render the primary `weaveffi.py` source by composing the shared - /// [`LanguageBackend::emit_members`] walk over every module. Shared by the - /// [`LanguageBackend::files`] hook and the test-facing - /// `render_python_module` wrapper so there is one assembly path. + /// [`LanguageBackend::emit_members`] walk over every module. fn render_py_source( &self, model: &BindingModel, @@ -365,19 +360,3 @@ impl LanguageBackend for PythonGenerator { Some(files) } } - -weaveffi_core::impl_generator_via_backend!(PythonGenerator); - -/// Render the `weaveffi.py` module source. Thin wrapper over the shared -/// [`LanguageBackend::emit_members`] walk (via -/// [`PythonGenerator::render_py_source`]); retained for direct use in tests. -#[cfg(test)] -fn render_python_module( - api: &ResolvedApi, - strip_module_prefix: bool, - prefix: &str, - input_basename: &str, -) -> String { - let model = BindingModel::build(api, prefix); - PythonGenerator.render_py_source(&model, strip_module_prefix, input_basename) -} diff --git a/crates/weaveffi-gen-python/src/runtime.rs b/crates/weaveffi-gen-python/src/runtime.rs index 7d5cdb4e..bc224a3a 100644 --- a/crates/weaveffi-gen-python/src/runtime.rs +++ b/crates/weaveffi-gen-python/src/runtime.rs @@ -1,6 +1,46 @@ //! The emitted Python runtime prelude: error base classes, library loading, -//! the owned-pointer helpers, and the `_BufferWriter`/`_BufferReader` pair -//! implementing the value-buffer wire format. +//! the ABI-revision check, the owned-pointer helpers, and the +//! `_BufferWriter`/`_BufferReader` pair implementing the value-buffer wire +//! format. + +use weaveffi_core::cabi::ABI_VERSION; + +/// Emit the load-time ABI-revision check that runs right after `_lib` is +/// opened and before any other symbol is bound. A missing symbol means the +/// producer predates ABI versioning; a different value means it was built +/// against an incompatible runtime. Both raise `ImportError` so the failure +/// surfaces at `import` time with a message naming both revisions. +fn render_abi_version_check(out: &mut String) { + out.push_str(&format!( + r#" +# The ABI revision these bindings were generated against. Checked before any +# other symbol is bound so a mismatched producer fails at import time instead +# of misreading the error struct or a value buffer later. +_ABI_VERSION = {ABI_VERSION} + + +def _check_abi_version(lib: ctypes.CDLL) -> None: + try: + fn = lib.weaveffi_abi_version + except AttributeError: + raise ImportError( + "the loaded WeaveFFI library predates ABI versioning " + f"(these bindings expect ABI revision {{_ABI_VERSION}})" + ) from None + fn.argtypes = [] + fn.restype = ctypes.c_uint32 + found = fn() + if found != _ABI_VERSION: + raise ImportError( + f"WeaveFFI ABI mismatch: these bindings expect revision {{_ABI_VERSION}} " + f"but the loaded library reports revision {{found}}" + ) + + +_check_abi_version(_lib) +"# + )); +} /// Append the fixed runtime prelude every generated `weaveffi.py` starts /// with. @@ -50,7 +90,11 @@ def _load_library() -> ctypes.CDLL: _lib = _load_library() -_lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] +"#, + ); + render_abi_version_check(out); + out.push_str( + r#"_lib.weaveffi_error_clear.argtypes = [ctypes.POINTER(_WeaveFFIErrorStruct)] _lib.weaveffi_error_clear.restype = None # Async completion callbacks receive a heap-boxed error the consumer owns; # weaveffi_error_free releases the message, the payload, and the box. diff --git a/crates/weaveffi-gen-python/src/stubs.rs b/crates/weaveffi-gen-python/src/stubs.rs index 46f2b2ff..16d52920 100644 --- a/crates/weaveffi-gen-python/src/stubs.rs +++ b/crates/weaveffi-gen-python/src/stubs.rs @@ -10,7 +10,8 @@ use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; use crate::docs::emit_doc; use crate::entities::py_code_class_name; use crate::types::{ - py_callable_hint, py_field, py_member_name, py_name, py_type_hint, py_wrapper_fn_name, + py_callable_hint, py_field, py_member_name, py_name, py_type_hint, py_variant, + py_wrapper_fn_name, }; /// Render the full `weaveffi.pyi` stub for the model. @@ -157,7 +158,7 @@ fn render_pyi_enum(out: &mut String, e: &EnumBinding) { out.push_str(&format!("class {}(IntEnum):\n", e.name)); for v in &e.variants { emit_doc(out, &v.doc, " "); - out.push_str(&format!(" {}: int\n", v.name)); + out.push_str(&format!(" {}: int\n", py_variant(&v.name))); } } @@ -173,16 +174,19 @@ fn render_pyi_rich_enum(out: &mut String, e: &EnumBinding) { out.push_str(" class Tag(IntEnum):\n"); for v in &e.variants { emit_doc(out, &v.doc, " "); - out.push_str(&format!(" {}: int\n", v.name)); + out.push_str(&format!(" {}: int\n", py_variant(&v.name))); } for v in &e.variants { - out.push_str(&format!(" {}: Type[\"{name}{}\"]\n", v.name, v.name)); + out.push_str(&format!( + " {0}: Type[\"{name}{0}\"]\n", + py_variant(&v.name) + )); } out.push_str(&format!( " @property\n def tag(self) -> \"{name}.Tag\": ...\n" )); for v in &e.variants { - let class = format!("{name}{}", v.name); + let class = format!("{name}{}", py_variant(&v.name)); out.push('\n'); emit_doc(out, &v.doc, ""); out.push_str(&format!("class {class}({name}):\n")); diff --git a/crates/weaveffi-gen-python/src/tests.rs b/crates/weaveffi-gen-python/src/tests.rs deleted file mode 100644 index aedfc3a5..00000000 --- a/crates/weaveffi-gen-python/src/tests.rs +++ /dev/null @@ -1,5109 +0,0 @@ -//! Output-content tests for the Python generator: these assert on the -//! generated source text and are the byte-compatibility guard for the -//! crate. - -use super::*; -use crate::stubs::render_pyi_module; -use crate::types::{py_ctypes_scalar, py_type_hint}; -use camino::Utf8Path; -use weaveffi_core::codegen::Generator; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_ir::ir::{ - Api, EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField, TypeRef, -}; - -fn make_api(modules: Vec) -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules, - generators: None, - package: None, - }) -} - -fn simple_module(functions: Vec) -> Module { - Module { - name: "math".into(), - functions, - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - } -} - -#[test] -fn generator_name_is_python() { - assert_eq!(Generator::name(&PythonGenerator), "python"); -} - -fn ping_api() -> ResolvedApi { - make_api(vec![simple_module(vec![Function { - name: "ping".into(), - params: vec![], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }])]) -} - -#[test] -fn python_keywords_escape_with_trailing_underscore() { - let mut module = simple_module(vec![Function { - name: "import".into(), - params: vec![Param { - name: "class".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }]); - module.structs = vec![StructDef { - name: "Event".into(), - doc: None, - fields: vec![StructField { - name: "from".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }]; - let api = make_api(vec![module]); - let out = render_python_module(&api, true, "weaveffi", "test.yml"); - // A reserved function or parameter name gains a trailing underscore, and - // the raw C call still targets the unescaped symbol. - assert!( - out.contains("def import_(class_: int) -> int:"), - "escaped signature missing: {out}" - ); - assert!(out.contains("_fn = _lib.weaveffi_math_import"), "{out}"); - assert!( - out.contains("_result = _fn(class_, ctypes.byref(_err))"), - "{out}" - ); - // A reserved field name is escaped consistently in the dataclass and in - // both codec directions. - assert!(out.contains(" from_: str"), "{out}"); - assert!(out.contains("_w.write_string(value.from_)"), "{out}"); - assert!(out.contains("from_=_r.read_string(),"), "{out}"); - // The stub mirrors the escaped spellings. - let model = BindingModel::build(&api, "weaveffi"); - let pyi = render_pyi_module(&model, true, "test.yml"); - assert!( - pyi.contains("def import_(class_: int) -> int: ..."), - "{pyi}" - ); - assert!(pyi.contains(" from_: str"), "{pyi}"); -} - -#[test] -fn package_emits_per_platform_trees_and_swaps_loader() { - use weaveffi_core::package::{FileContent, PackageContext}; - use weaveffi_core::platform::{BinarySet, Platform}; - - let api = ping_api(); - let model = BindingModel::build(&api, "weaveffi"); - let mut bins = BinarySet::new("calculator"); - bins.insert( - Platform::MacosArm64, - "/src/darwin-arm64/libcalculator.dylib", - ); - bins.insert(Platform::LinuxX64, "/src/linux-x64/libcalculator.so"); - let ctx = PackageContext { - binaries: &bins, - input_basename: Some("calculator.yml"), - }; - let files = LanguageBackend::package( - &PythonGenerator, - &api, - &model, - &ctx, - Utf8Path::new("/out"), - &PythonConfig::default(), - ) - .expect("python supports packaging"); - - // A complete wheel tree per bundled platform. - assert!(files - .iter() - .any(|f| f.path.as_str().contains("python/darwin-arm64/"))); - assert!(files - .iter() - .any(|f| f.path.as_str().contains("python/linux-x64/"))); - // Exactly one bundled binary per platform, materialized as copies. - assert_eq!(files.iter().filter(|f| f.is_binary()).count(), 2); - - // The loader was rewritten to prefer the bundled library (the fragile - // string replace must keep matching the generator's loader block). - let py = files - .iter() - .find(|f| { - f.path - .as_str() - .ends_with("darwin-arm64/weaveffi/weaveffi.py") - }) - .expect("weaveffi.py present"); - let FileContent::Text(src) = &py.content else { - panic!("weaveffi.py should be text"); - }; - assert!( - src.contains("os.path.exists") && src.contains("libcalculator.dylib"), - "packaged loader not applied: {src}" - ); - assert!( - !src.contains("\"libweaveffi.dylib\""), - "generate-mode loader leaked into the package" - ); -} - -#[test] -fn generate_creates_output_files() { - let api = make_api(vec![simple_module(vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }])]); - - let tmp = std::env::temp_dir().join("weaveffi_test_python_gen_output"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - PythonGenerator - .generate( - &api, - out_dir, - &PythonConfig { - strip_module_prefix: true, - ..PythonConfig::default() - }, - ) - .unwrap(); - - let init = std::fs::read_to_string(tmp.join("python/weaveffi/__init__.py")).unwrap(); - assert!(init.contains("from .weaveffi import *")); - - let weaveffi = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap(); - assert!(weaveffi.contains("WeaveFFI")); - assert!(weaveffi.contains("def add(")); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn output_files_lists_all() { - let api = make_api(vec![]); - let out = Utf8Path::new("/tmp/out"); - let files = PythonGenerator.output_files(&api, out, &PythonConfig::default()); - assert_eq!( - files, - vec![ - format!("{out}/python/README.md"), - format!("{out}/python/pyproject.toml"), - format!("{out}/python/setup.py"), - format!("{out}/python/weaveffi/__init__.py"), - format!("{out}/python/weaveffi/weaveffi.py"), - format!("{out}/python/weaveffi/weaveffi.pyi"), - ] - ); -} - -#[test] -fn preamble_has_load_library() { - let api = make_api(vec![]); - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!(py.contains("def _load_library()"), "missing _load_library"); - assert!( - py.contains("libweaveffi.dylib"), - "missing macOS library name" - ); - assert!(py.contains("libweaveffi.so"), "missing Linux library name"); - assert!(py.contains("weaveffi.dll"), "missing Windows library name"); - assert!(py.contains("ctypes.CDLL(name)"), "missing CDLL call"); -} - -#[test] -fn preamble_has_error_handling() { - let api = make_api(vec![]); - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("class WeaveFFIError(Exception):"), - "missing error class" - ); - assert!( - py.contains("class _WeaveFFIErrorStruct(ctypes.Structure):"), - "missing error struct" - ); - assert!(py.contains("def _check_error("), "missing _check_error"); - assert!( - py.contains("weaveffi_error_clear"), - "missing error_clear setup" - ); -} - -#[test] -fn simple_i32_function() { - let api = make_api(vec![simple_module(vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }])]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("def add(a: int, b: int) -> int:"), - "missing function signature: {py}" - ); - assert!( - py.contains("_lib.weaveffi_math_add"), - "missing C symbol: {py}" - ); - assert!( - py.contains("ctypes.c_int32, ctypes.c_int32"), - "missing argtypes: {py}" - ); - assert!( - py.contains("_fn.restype = ctypes.c_int32"), - "missing restype: {py}" - ); - assert!( - py.contains("_check_error(_err)"), - "missing error check: {py}" - ); - assert!(py.contains("return _result"), "missing return: {py}"); -} - -#[test] -fn string_function_encode_decode() { - let api = make_api(vec![Module { - name: "text".into(), - functions: vec![Function { - name: "echo".into(), - params: vec![Param { - name: "msg".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("def echo(msg: str) -> str:"), - "missing signature: {py}" - ); - assert!(py.contains("ctypes.c_char_p"), "missing c_char_p: {py}"); - assert!( - py.contains("_string_to_bytes(msg)"), - "missing _string_to_bytes call: {py}" - ); - // The owned return string is copied and released via `_take_string`. - assert!( - py.contains("return _take_string(_result) or \"\""), - "missing _take_string call: {py}" - ); -} - -#[test] -fn void_function() { - let api = make_api(vec![simple_module(vec![Function { - name: "reset".into(), - params: vec![], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }])]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("def reset() -> None:"), - "missing void signature: {py}" - ); - assert!( - py.contains("_fn.restype = None"), - "missing None restype: {py}" - ); - assert!( - !py.contains("_result ="), - "void function should not assign _result: {py}" - ); -} - -#[test] -fn enum_intenum_class() { - let api = make_api(vec![Module { - name: "paint".into(), - functions: vec![], - structs: vec![], - enums: vec![EnumDef { - name: "Color".into(), - doc: Some("Primary colors".into()), - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Blue".into(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("class Color(IntEnum):"), - "missing IntEnum class: {py}" - ); - assert!( - py.contains("\"\"\"Primary colors\"\"\""), - "missing doc: {py}" - ); - assert!(py.contains("Red = 0"), "missing Red: {py}"); - assert!(py.contains("Green = 1"), "missing Green: {py}"); - assert!(py.contains("Blue = 2"), "missing Blue: {py}"); -} - -#[test] -fn enum_param_and_return() { - let api = make_api(vec![Module { - name: "paint".into(), - functions: vec![Function { - name: "mix".into(), - params: vec![Param { - name: "a".into(), - ty: TypeRef::Enum("Color".into()), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Enum("Color".into())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!(py.contains("a: \"Color\""), "missing enum param hint: {py}"); - assert!( - py.contains("-> \"Color\":"), - "missing enum return hint: {py}" - ); - assert!(py.contains("a.value"), "missing .value conversion: {py}"); - assert!( - py.contains("return Color(_result)"), - "missing enum return wrap: {py}" - ); -} - -#[test] -fn struct_dataclass_value_type() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "age".into(), - ty: TypeRef::I32, - doc: None, - }, - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("@dataclass\nclass Contact:"), - "missing dataclass: {py}" - ); - assert!(py.contains("name: str"), "missing name field: {py}"); - assert!(py.contains("age: int"), "missing age field: {py}"); - // Records are plain value types: no pointer wrapping, no destructor, - // and no C symbols at all. - assert!(!py.contains("self._ptr"), "no pointer wrapping: {py}"); - assert!( - !py.contains("Contact_destroy"), - "no destroy symbol for records: {py}" - ); - assert!( - !py.contains("Contact_get_"), - "no getter symbols for records: {py}" - ); - // The buffer codecs are generated beside the dataclass. - assert!( - py.contains("def _write_Contact(_w: _BufferWriter, value: \"Contact\") -> None:"), - "missing field writer: {py}" - ); - assert!( - py.contains("_w.write_string(value.name)"), - "missing string field write: {py}" - ); - assert!( - py.contains("_w.write_i32(value.age)"), - "missing i32 field write: {py}" - ); - assert!( - py.contains("def _read_Contact(_r: _BufferReader) -> \"Contact\":"), - "missing field reader: {py}" - ); - assert!( - py.contains("name=_r.read_string(),"), - "missing string field read: {py}" - ); - assert!( - py.contains("age=_r.read_i32(),"), - "missing i32 field read: {py}" - ); - assert!( - py.contains("def _pack_Contact(value: \"Contact\") -> bytes:"), - "missing pack helper: {py}" - ); - assert!( - py.contains("def _unpack_Contact(data: bytes) -> \"Contact\":"), - "missing unpack helper: {py}" - ); -} - -#[test] -fn buffered_record_param_packs() { - let api = ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules: vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "save_contact".into(), - params: vec![Param { - name: "contact".into(), - ty: TypeRef::Record("Contact".into()), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "age".into(), - ty: TypeRef::I32, - doc: None, - }, - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }); - let dir = tempfile::tempdir().unwrap(); - let out = Utf8Path::from_path(dir.path()).unwrap(); - PythonGenerator - .generate(&api, out, &PythonConfig::default()) - .unwrap(); - let py = std::fs::read_to_string(out.join("python/weaveffi/weaveffi.py")).unwrap(); - // The caller packs the record to bytes and passes the (ptr, len) - // slot pair; `c_char_p` accepts the bytes directly. - assert!( - py.contains("def save_contact(contact: \"Contact\") -> None:"), - "missing wrapper signature: {py}" - ); - assert!( - py.contains("_contact_buf = _pack_Contact(contact)"), - "missing pack call: {py}" - ); - assert!( - py.contains( - "_fn.argtypes = [ctypes.c_char_p, ctypes.c_size_t, \ - ctypes.POINTER(_WeaveFFIErrorStruct)]" - ), - "missing buffered argtypes: {py}" - ); - assert!( - py.contains("_fn(_contact_buf, len(_contact_buf), ctypes.byref(_err))"), - "missing buffered call args: {py}" - ); - // No builder class survives the migration. - assert!(!py.contains("ContactBuilder"), "builders are gone: {py}"); -} - -#[test] -fn struct_return() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "get_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Handle, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("-> \"Contact\":"), - "missing struct return hint: {py}" - ); - // A buffered return keeps its raw address plus a trailing out_len. - assert!( - py.contains("_fn.restype = ctypes.c_void_p"), - "missing void_p restype for buffered return: {py}" - ); - assert!( - py.contains("_out_len = ctypes.c_size_t(0)"), - "missing out_len for buffered return: {py}" - ); - // The owned encoded buffer is copied, freed, then decoded. - assert!( - py.contains("_data = _take_buffer(_result, _out_len.value)"), - "missing buffer take: {py}" - ); - assert!( - py.contains("return _unpack_Contact(_data)"), - "missing record decode: {py}" - ); -} - -#[test] -fn bool_uses_c_int32() { - let api = make_api(vec![simple_module(vec![Function { - name: "is_valid".into(), - params: vec![Param { - name: "flag".into(), - ty: TypeRef::Bool, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Bool), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }])]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!(py.contains("flag: bool"), "missing bool param: {py}"); - assert!(py.contains("-> bool:"), "missing bool return: {py}"); - assert!( - py.contains("ctypes.c_int32"), - "missing c_int32 for Bool: {py}" - ); - assert!( - py.contains("1 if flag else 0"), - "missing bool-to-int conversion: {py}" - ); - assert!( - py.contains("return bool(_result)"), - "missing int-to-bool conversion: {py}" - ); -} - -#[test] -fn handle_uses_c_uint64() { - let api = make_api(vec![simple_module(vec![Function { - name: "create".into(), - params: vec![], - returns: Some(TypeRef::Handle), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }])]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("ctypes.c_uint64"), - "missing c_uint64 for Handle: {py}" - ); -} - -#[test] -fn bytes_param_and_return() { - let api = make_api(vec![Module { - name: "store".into(), - functions: vec![Function { - name: "process".into(), - params: vec![Param { - name: "data".into(), - ty: TypeRef::Bytes, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Bytes), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!(py.contains("data: bytes"), "missing bytes param: {py}"); - assert!(py.contains("-> bytes:"), "missing bytes return: {py}"); - assert!( - py.contains("ctypes.POINTER(ctypes.c_uint8)"), - "missing uint8 pointer: {py}" - ); - assert!(py.contains("ctypes.c_size_t"), "missing size_t: {py}"); - assert!(py.contains("_out_len"), "missing out_len: {py}"); -} - -#[test] -fn optional_value_param_and_return() { - let api = make_api(vec![Module { - name: "store".into(), - functions: vec![Function { - name: "find".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Optional(Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Optional(Box::new(TypeRef::I32))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("id: Optional[int]"), - "missing optional param: {py}" - ); - assert!( - py.contains("-> Optional[int]:"), - "missing optional return: {py}" - ); - // The optional packs into a per-parameter value buffer: a flag byte, - // then the value when present. - assert!( - py.contains("_id_w = _BufferWriter()"), - "missing param writer: {py}" - ); - assert!(py.contains("if id is None:"), "missing None branch: {py}"); - assert!( - py.contains("_id_w.write_option_flag(False)"), - "missing absent flag write: {py}" - ); - assert!( - py.contains("_id_w.write_option_flag(True)"), - "missing present flag write: {py}" - ); - assert!( - py.contains("_id_w.write_i32(id)"), - "missing value write: {py}" - ); - assert!( - py.contains("_id_buf = _id_w.finish()"), - "missing buffer finish: {py}" - ); - assert!( - py.contains("_fn(_id_buf, len(_id_buf), ctypes.byref(_out_len), ctypes.byref(_err))"), - "missing buffered call args: {py}" - ); - // The buffered return is copied, freed, and decoded through the - // option flag. - assert!( - py.contains("_data = _take_buffer(_result, _out_len.value)"), - "missing buffer take: {py}" - ); - assert!( - py.contains( - "return _decode_buffer(_data, \ - lambda _r: (_r.read_i32() if _r.read_option_flag() else None))" - ), - "missing optional decode: {py}" - ); -} - -#[test] -fn optional_string_return() { - let api = make_api(vec![Module { - name: "store".into(), - functions: vec![Function { - name: "get_name".into(), - params: vec![], - returns: Some(TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("-> Optional[str]:"), - "missing optional str return: {py}" - ); - // An optional string is buffered: the encoded value is copied, - // freed, and decoded through the option flag. - assert!( - py.contains("_data = _take_buffer(_result, _out_len.value)"), - "missing buffer take for optional string: {py}" - ); - assert!( - py.contains( - "return _decode_buffer(_data, \ - lambda _r: (_r.read_string() if _r.read_option_flag() else None))" - ), - "missing optional string decode: {py}" - ); -} - -#[test] -fn list_param_and_return() { - let api = make_api(vec![Module { - name: "batch".into(), - functions: vec![ - Function { - name: "process".into(), - params: vec![Param { - name: "ids".into(), - ty: TypeRef::List(Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "get_ids".into(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::I32))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!(py.contains("ids: List[int]"), "missing list param: {py}"); - assert!(py.contains("-> List[int]:"), "missing list return: {py}"); - // The list packs into a value buffer: a count, then each element. - assert!( - py.contains("_ids_w.write_len(len(ids))"), - "missing list length write: {py}" - ); - assert!(py.contains("for _e0 in ids:"), "missing element loop: {py}"); - assert!( - py.contains("_ids_w.write_i32(_e0)"), - "missing element write: {py}" - ); - assert!( - py.contains("_out_len"), - "missing out_len for list return: {py}" - ); - assert!( - py.contains("[_r.read_i32() for _i0 in range(_r.read_len())]"), - "missing list decode: {py}" - ); -} - -#[test] -fn map_param_and_return() { - let api = make_api(vec![Module { - name: "store".into(), - functions: vec![ - Function { - name: "update".into(), - params: vec![Param { - name: "scores".into(), - ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "get_scores".into(), - params: vec![], - returns: Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("scores: Dict[str, int]"), - "missing map param: {py}" - ); - assert!( - py.contains("-> Dict[str, int]:"), - "missing map return: {py}" - ); - // The map packs into a value buffer: a count, then alternating - // key and value. - assert!( - py.contains("_scores_w.write_len(len(scores))"), - "missing map length write: {py}" - ); - assert!( - py.contains("for _k0, _v0 in scores.items():"), - "missing entry loop: {py}" - ); - assert!( - py.contains("_scores_w.write_string(_k0)"), - "missing key write: {py}" - ); - assert!( - py.contains("_scores_w.write_i32(_v0)"), - "missing value write: {py}" - ); - assert!( - py.contains("dict((_r.read_string(), _r.read_i32()) for _i0 in range(_r.read_len()))"), - "missing map decode: {py}" - ); -} - -#[test] -fn struct_optional_string_field() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("email: Optional[str]"), - "missing optional field annotation: {py}" - ); - // The codec writes a flag byte, then the value only when present. - assert!( - py.contains("if value.email is None:"), - "missing None branch in writer: {py}" - ); - assert!( - py.contains("_w.write_option_flag(False)"), - "missing absent flag write: {py}" - ); - assert!( - py.contains("_w.write_string(value.email)"), - "missing present value write: {py}" - ); - assert!( - py.contains("email=(_r.read_string() if _r.read_option_flag() else None),"), - "missing optional field read: {py}" - ); -} - -#[test] -fn struct_enum_field() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "role".into(), - ty: TypeRef::Enum("Role".into()), - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("role: \"Role\""), - "missing enum field annotation: {py}" - ); - // C-style enum fields serialize as their i32 discriminant. - assert!( - py.contains("_w.write_i32(value.role)"), - "missing enum field write: {py}" - ); - assert!( - py.contains("role=Role(_r.read_i32()),"), - "missing enum field read: {py}" - ); -} - -#[test] -fn comprehensive_contacts_api() { - let api = make_api(vec![Module { - name: "contacts".into(), - enums: vec![EnumDef { - name: "ContactType".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Personal".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Work".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: None, - }, - StructField { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - doc: None, - }, - StructField { - name: "contact_type".into(), - ty: TypeRef::Enum("ContactType".into()), - doc: None, - }, - ], - }], - functions: vec![ - Function { - name: "create_contact".into(), - params: vec![ - Param { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }, - Param { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - mutable: false, - doc: None, - }, - Param { - name: "contact_type".into(), - ty: TypeRef::Enum("ContactType".into()), - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::Handle), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "get_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Handle, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "list_contacts".into(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::Record("Contact".into())))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "count_contacts".into(), - params: vec![], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let tmp = std::env::temp_dir().join("weaveffi_test_python_gen_contacts"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - PythonGenerator - .generate( - &api, - out_dir, - &PythonConfig { - strip_module_prefix: true, - ..PythonConfig::default() - }, - ) - .unwrap(); - - let py = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap(); - - assert!(py.contains("class ContactType(IntEnum):")); - assert!(py.contains("Personal = 0")); - assert!(py.contains("Work = 1")); - - assert!(py.contains("@dataclass\nclass Contact:")); - assert!(py.contains("id: int")); - assert!(py.contains("first_name: str")); - assert!(py.contains("email: Optional[str]")); - assert!(py.contains("contact_type: \"ContactType\"")); - // Records carry no C symbols; only the buffer codecs exist. - assert!(!py.contains("Contact_destroy")); - assert!(!py.contains("Contact_get_")); - assert!(py.contains("def _pack_Contact(")); - assert!(py.contains("def _unpack_Contact(")); - - assert!(py.contains("def create_contact(")); - assert!(py.contains("weaveffi_contacts_create_contact")); - assert!(py.contains("def get_contact(id: int) -> \"Contact\":")); - assert!(py.contains("def list_contacts() -> List[\"Contact\"]:")); - assert!(py.contains("def count_contacts() -> int:")); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn type_hint_mapping() { - assert_eq!(py_type_hint(&TypeRef::I32), "int"); - assert_eq!(py_type_hint(&TypeRef::U32), "int"); - assert_eq!(py_type_hint(&TypeRef::I64), "int"); - assert_eq!(py_type_hint(&TypeRef::F64), "float"); - assert_eq!(py_type_hint(&TypeRef::Bool), "bool"); - assert_eq!(py_type_hint(&TypeRef::StringUtf8), "str"); - assert_eq!(py_type_hint(&TypeRef::Bytes), "bytes"); - assert_eq!(py_type_hint(&TypeRef::Handle), "int"); - assert_eq!(py_type_hint(&TypeRef::Record("Foo".into())), "\"Foo\""); - assert_eq!(py_type_hint(&TypeRef::Enum("Bar".into())), "\"Bar\""); - // A typed handle is an opaque pointer-sized token surfacing as a - // plain int, matching the untyped handle. - assert_eq!(py_type_hint(&TypeRef::TypedHandle("Foo".into())), "int"); - assert_eq!( - py_type_hint(&TypeRef::TypedHandle("kv.Store".into())), - "int" - ); - // Cross-module references (resolved to a qualified IR name) must still - // annotate the bare *local* class, which is the only symbol that exists - // in the generated module. - assert_eq!( - py_type_hint(&TypeRef::Record("kv.Store".into())), - "\"Store\"" - ); - assert_eq!(py_type_hint(&TypeRef::Enum("kv.Kind".into())), "\"Kind\""); - assert_eq!( - py_type_hint(&TypeRef::Optional(Box::new(TypeRef::I32))), - "Optional[int]" - ); - assert_eq!( - py_type_hint(&TypeRef::List(Box::new(TypeRef::I32))), - "List[int]" - ); - assert_eq!( - py_type_hint(&TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32) - )), - "Dict[str, int]" - ); -} - -#[test] -fn ctypes_scalar_mapping() { - assert_eq!(py_ctypes_scalar(&TypeRef::I32), "ctypes.c_int32"); - assert_eq!(py_ctypes_scalar(&TypeRef::U32), "ctypes.c_uint32"); - assert_eq!(py_ctypes_scalar(&TypeRef::I64), "ctypes.c_int64"); - assert_eq!(py_ctypes_scalar(&TypeRef::F64), "ctypes.c_double"); - assert_eq!(py_ctypes_scalar(&TypeRef::Bool), "ctypes.c_int32"); - assert_eq!(py_ctypes_scalar(&TypeRef::StringUtf8), "ctypes.c_char_p"); - assert_eq!(py_ctypes_scalar(&TypeRef::Handle), "ctypes.c_uint64"); - assert_eq!(py_ctypes_scalar(&TypeRef::Bytes), "ctypes.c_uint8"); - assert_eq!( - py_ctypes_scalar(&TypeRef::TypedHandle("X".into())), - "ctypes.c_void_p" - ); - assert_eq!( - py_ctypes_scalar(&TypeRef::Enum("X".into())), - "ctypes.c_int32" - ); -} - -#[test] -fn list_struct_return() { - let api = make_api(vec![Module { - name: "store".into(), - functions: vec![Function { - name: "list_items".into(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::Record("Item".into())))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("-> List[\"Item\"]:"), - "missing list struct return: {py}" - ); - // Each element decodes through the record's reader. - assert!( - py.contains("[_read_Item(_r) for _i0 in range(_r.read_len())]"), - "missing record list decode: {py}" - ); -} - -#[test] -fn struct_bytes_field() { - let api = make_api(vec![Module { - name: "storage".into(), - functions: vec![], - structs: vec![StructDef { - name: "Blob".into(), - doc: None, - fields: vec![StructField { - name: "data".into(), - ty: TypeRef::Bytes, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("data: bytes"), - "missing bytes field annotation: {py}" - ); - assert!( - py.contains("_w.write_bytes(value.data)"), - "missing bytes field write: {py}" - ); - assert!( - py.contains("data=_r.read_bytes(),"), - "missing bytes field read: {py}" - ); -} - -#[test] -fn python_generates_type_stubs() { - let api = make_api(vec![Module { - name: "contacts".into(), - enums: vec![EnumDef { - name: "ContactType".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Personal".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Work".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: None, - }, - StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - doc: None, - }, - StructField { - name: "tags".into(), - ty: TypeRef::List(Box::new(TypeRef::StringUtf8)), - doc: None, - }, - StructField { - name: "metadata".into(), - ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - doc: None, - }, - ], - }], - functions: vec![ - Function { - name: "create_contact".into(), - params: vec![ - Param { - name: "name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }, - Param { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::Handle), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "get_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Handle, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "delete_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Handle, - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let tmp = std::env::temp_dir().join("weaveffi_test_python_pyi"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - PythonGenerator - .generate( - &api, - out_dir, - &PythonConfig { - strip_module_prefix: true, - ..PythonConfig::default() - }, - ) - .unwrap(); - - let pyi_path = tmp.join("python/weaveffi/weaveffi.pyi"); - assert!(pyi_path.exists(), ".pyi file must exist"); - - let pyi = std::fs::read_to_string(&pyi_path).unwrap(); - - assert!( - pyi.contains("from enum import IntEnum"), - "missing IntEnum import" - ); - assert!( - pyi.contains("from typing import Callable, Dict, Iterator, List, Optional"), - "missing typing imports" - ); - - assert!( - pyi.contains("class ContactType(IntEnum):"), - "missing enum stub" - ); - assert!( - pyi.contains(" Personal: int"), - "missing enum variant Personal" - ); - assert!(pyi.contains(" Work: int"), "missing enum variant Work"); - - assert!(pyi.contains("class Contact:"), "missing struct stub"); - assert!(pyi.contains(" id: int\n"), "missing id field: {pyi}"); - assert!(pyi.contains(" name: str\n"), "missing name field: {pyi}"); - assert!( - pyi.contains(" email: Optional[str]\n"), - "missing email field: {pyi}" - ); - assert!( - pyi.contains(" tags: List[str]\n"), - "missing tags field: {pyi}" - ); - assert!( - pyi.contains(" metadata: Dict[str, int]\n"), - "missing metadata field: {pyi}" - ); - assert!( - pyi.contains( - " def __init__(self, id: int, name: str, email: Optional[str], \ - tags: List[str], metadata: Dict[str, int]) -> None: ..." - ), - "missing dataclass constructor stub: {pyi}" - ); - - assert!( - pyi.contains("def create_contact(name: str, email: Optional[str]) -> int: ..."), - "missing create_contact stub: {pyi}" - ); - assert!( - pyi.contains("def get_contact(id: int) -> \"Contact\": ..."), - "missing get_contact stub: {pyi}" - ); - assert!( - pyi.contains("def delete_contact(id: int) -> None: ..."), - "missing delete_contact stub: {pyi}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn generate_python_basic() { - let api = make_api(vec![simple_module(vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }])]); - - let tmp = std::env::temp_dir().join("weaveffi_test_py_basic"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - PythonGenerator - .generate( - &api, - out_dir, - &PythonConfig { - strip_module_prefix: true, - ..PythonConfig::default() - }, - ) - .unwrap(); - - let py = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap(); - - assert!(py.contains("def add(a: int, b: int) -> int:")); - assert!(py.contains("_fn = _lib.weaveffi_math_add")); - assert!(py.contains("ctypes.c_int32, ctypes.c_int32")); - assert!(py.contains("_fn.restype = ctypes.c_int32")); - assert!(py.contains("_err = _WeaveFFIErrorStruct()")); - assert!(py.contains("_check_error(_err)")); - assert!(py.contains("return _result")); - - assert!(py.contains("import ctypes")); - assert!(py.contains("from enum import IntEnum")); - assert!(py.contains("from typing import Callable, Dict, Iterator, List, Optional")); - assert!(py.contains("class WeaveFFIError(Exception):")); - assert!(py.contains("def _load_library()")); - assert!(py.contains("_lib = _load_library()")); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn generate_python_with_structs() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: Some("A contact record".into()), - fields: vec![ - StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: None, - }, - StructField { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "last_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - doc: None, - }, - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - - assert!( - py.contains("@dataclass\nclass Contact:"), - "missing dataclass decl" - ); - assert!( - py.contains("\"\"\"A contact record\"\"\""), - "missing doc: {py}" - ); - // Value type: no pointer wrapping, no destructor, no getters. - assert!(!py.contains("self._ptr = _ptr")); - assert!(!py.contains("weaveffi_contacts_Contact_destroy")); - assert!(!py.contains("weaveffi_contacts_Contact_get_id")); - - assert!(py.contains(" id: int")); - assert!(py.contains(" first_name: str")); - assert!(py.contains(" last_name: str")); - assert!(py.contains(" email: Optional[str]")); - - // The codec functions serialize fields in declaration order. - assert!(py.contains("_w.write_i64(value.id)")); - assert!(py.contains("_w.write_string(value.first_name)")); - assert!(py.contains("id=_r.read_i64(),")); - assert!(py.contains("email=(_r.read_string() if _r.read_option_flag() else None),")); -} - -#[test] -fn generate_python_with_enums() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "get_type".into(), - params: vec![Param { - name: "ct".into(), - ty: TypeRef::Enum("ContactType".into()), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Enum("ContactType".into())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![EnumDef { - name: "ContactType".into(), - doc: Some("Type of contact".into()), - variants: vec![ - EnumVariant { - name: "Personal".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Work".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Other".into(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - - assert!(py.contains("class ContactType(IntEnum):")); - assert!(py.contains("\"\"\"Type of contact\"\"\"")); - assert!(py.contains("Personal = 0")); - assert!(py.contains("Work = 1")); - assert!(py.contains("Other = 2")); - - assert!( - py.contains("ct: \"ContactType\""), - "missing enum param hint" - ); - assert!( - py.contains("-> \"ContactType\":"), - "missing enum return hint" - ); - assert!(py.contains("ct.value"), "missing .value for enum param"); - assert!( - py.contains("return ContactType(_result)"), - "missing enum return wrap" - ); - assert!(py.contains("ctypes.c_int32"), "enum should use c_int32 ABI"); -} - -#[test] -fn generate_python_with_optionals() { - let api = make_api(vec![Module { - name: "store".into(), - functions: vec![ - Function { - name: "find_int".into(), - params: vec![Param { - name: "key".into(), - ty: TypeRef::Optional(Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Optional(Box::new(TypeRef::I32))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "find_name".into(), - params: vec![Param { - name: "prefix".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "find_contact".into(), - params: vec![], - returns: Some(TypeRef::Optional(Box::new(TypeRef::Record( - "Contact".into(), - )))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "find_flag".into(), - params: vec![], - returns: Some(TypeRef::Optional(Box::new(TypeRef::Bool))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - - assert!( - py.contains("key: Optional[int]"), - "missing Optional[int] param" - ); - assert!( - py.contains("-> Optional[int]:"), - "missing Optional[int] return" - ); - // Optional scalars pack into per-parameter value buffers. - assert!( - py.contains("_key_w.write_option_flag(False)"), - "missing absent flag write for key" - ); - assert!( - py.contains("_key_w.write_i32(key)"), - "missing present value write for key" - ); - - assert!( - py.contains("prefix: Optional[str]"), - "missing Optional[str] param" - ); - assert!( - py.contains("-> Optional[str]:"), - "missing Optional[str] return" - ); - assert!( - py.contains("_prefix_w.write_string(prefix)"), - "missing optional string write" - ); - - assert!( - py.contains("-> Optional[\"Contact\"]:"), - "missing Optional struct return" - ); - assert!( - py.contains("lambda _r: (_read_Contact(_r) if _r.read_option_flag() else None)"), - "missing optional struct decode" - ); - - assert!( - py.contains("-> Optional[bool]:"), - "missing Optional[bool] return" - ); - assert!( - py.contains("lambda _r: (_r.read_bool() if _r.read_option_flag() else None)"), - "missing optional bool decode" - ); -} - -#[test] -fn generate_python_with_lists() { - let api = make_api(vec![Module { - name: "batch".into(), - functions: vec![ - Function { - name: "process_ids".into(), - params: vec![Param { - name: "ids".into(), - ty: TypeRef::List(Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "get_names".into(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::StringUtf8))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "get_items".into(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::Record("Item".into())))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - - assert!(py.contains("ids: List[int]"), "missing List[int] param"); - // The list packs into a value buffer passed as a (ptr, len) pair. - assert!( - py.contains("_ids_w.write_len(len(ids))"), - "missing list length write" - ); - assert!( - py.contains("_ids_w.write_i32(_e0)"), - "missing list element write" - ); - assert!(py.contains("ctypes.c_size_t"), "missing size_t for length"); - - assert!( - py.contains("-> List[str]:"), - "missing List[str] return: {py}" - ); - assert!( - py.contains("[_r.read_string() for _i0 in range(_r.read_len())]"), - "missing string list decode: {py}" - ); - - assert!( - py.contains("-> List[\"Item\"]:"), - "missing List struct return" - ); - assert!( - py.contains("[_read_Item(_r) for _i0 in range(_r.read_len())]"), - "missing record list decode" - ); -} - -#[test] -fn generate_python_with_maps() { - let api = make_api(vec![Module { - name: "config".into(), - functions: vec![ - Function { - name: "set_config".into(), - params: vec![Param { - name: "settings".into(), - ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "get_config".into(), - params: vec![], - returns: Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - - assert!( - py.contains("settings: Dict[str, int]"), - "missing Dict param hint" - ); - // The map packs into a single value buffer: count, then alternating - // key and value. - assert!( - py.contains("_settings_w.write_len(len(settings))"), - "missing map length write" - ); - assert!( - py.contains("for _k0, _v0 in settings.items():"), - "missing entry loop" - ); - assert!( - py.contains("_settings_w.write_string(_k0)"), - "missing key write" - ); - assert!( - py.contains("_settings_w.write_i32(_v0)"), - "missing value write" - ); - assert!( - py.contains("_fn(_settings_buf, len(_settings_buf), ctypes.byref(_err))"), - "missing buffered call args" - ); - - assert!( - py.contains("-> Dict[str, int]:"), - "missing Dict return hint" - ); - assert!( - py.contains("_out_len = ctypes.c_size_t(0)"), - "missing out_len init" - ); - // The returned buffer is copied, freed, then decoded into a dict. - assert!( - py.contains("_data = _take_buffer(_result, _out_len.value)"), - "missing buffer take" - ); - assert!( - py.contains("dict((_r.read_string(), _r.read_i32()) for _i0 in range(_r.read_len()))"), - "missing map decode" - ); -} - -#[test] -fn generate_python_pyi_types() { - let api = make_api(vec![Module { - name: "contacts".into(), - enums: vec![EnumDef { - name: "ContactType".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Personal".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Work".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Other".into(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: None, - }, - StructField { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - doc: None, - }, - StructField { - name: "tags".into(), - ty: TypeRef::List(Box::new(TypeRef::StringUtf8)), - doc: None, - }, - StructField { - name: "scores".into(), - ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - doc: None, - }, - ], - }], - functions: vec![ - Function { - name: "create_contact".into(), - params: vec![ - Param { - name: "name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }, - Param { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::Handle), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "get_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Handle, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "list_contacts".into(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::Record("Contact".into())))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "delete_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Handle, - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let pyi = render_pyi_module(&BindingModel::build(&api, "weaveffi"), true, "weaveffi.yml"); - - assert!(pyi.contains("from enum import IntEnum")); - assert!(pyi.contains("from typing import Callable, Dict, Iterator, List, Optional")); - - assert!(pyi.contains("class ContactType(IntEnum):")); - assert!(pyi.contains(" Personal: int")); - assert!(pyi.contains(" Work: int")); - assert!(pyi.contains(" Other: int")); - - assert!(pyi.contains("class Contact:")); - assert!(pyi.contains(" id: int")); - assert!(pyi.contains(" first_name: str")); - assert!(pyi.contains(" email: Optional[str]")); - assert!(pyi.contains(" tags: List[str]")); - assert!(pyi.contains(" scores: Dict[str, int]")); - assert!(pyi.contains( - " def __init__(self, id: int, first_name: str, email: Optional[str], \ - tags: List[str], scores: Dict[str, int]) -> None: ..." - )); - - assert!(pyi.contains("def create_contact(name: str, email: Optional[str]) -> int: ...")); - assert!(pyi.contains("def get_contact(id: int) -> \"Contact\": ...")); - assert!(pyi.contains("def list_contacts() -> List[\"Contact\"]: ...")); - assert!(pyi.contains("def delete_contact(id: int) -> None: ...")); -} - -#[test] -fn generate_python_full_contacts() { - let api = make_api(vec![Module { - name: "contacts".into(), - enums: vec![EnumDef { - name: "ContactType".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Personal".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Work".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Other".into(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: None, - }, - StructField { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "last_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - doc: None, - }, - StructField { - name: "contact_type".into(), - ty: TypeRef::Enum("ContactType".into()), - doc: None, - }, - ], - }], - functions: vec![ - Function { - name: "create_contact".into(), - params: vec![ - Param { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }, - Param { - name: "last_name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }, - Param { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - mutable: false, - doc: None, - }, - Param { - name: "contact_type".into(), - ty: TypeRef::Enum("ContactType".into()), - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::Handle), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "get_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Handle, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "list_contacts".into(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::Record("Contact".into())))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "delete_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Handle, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Bool), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "count_contacts".into(), - params: vec![], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let tmp = std::env::temp_dir().join("weaveffi_test_py_full_contacts"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - PythonGenerator - .generate( - &api, - out_dir, - &PythonConfig { - strip_module_prefix: true, - ..PythonConfig::default() - }, - ) - .unwrap(); - - let py = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap(); - let pyi = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.pyi")).unwrap(); - - assert!(py.contains("class ContactType(IntEnum):")); - assert!(py.contains("Personal = 0")); - assert!(py.contains("Work = 1")); - assert!(py.contains("Other = 2")); - - assert!(py.contains("@dataclass\nclass Contact:")); - assert!(!py.contains("weaveffi_contacts_Contact_destroy")); - assert!(!py.contains("weaveffi_contacts_Contact_get_id")); - assert!(py.contains(" id: int")); - assert!(py.contains(" first_name: str")); - assert!(py.contains(" last_name: str")); - assert!(py.contains(" email: Optional[str]")); - assert!(py.contains(" contact_type: \"ContactType\"")); - assert!(py.contains("def _pack_Contact(")); - assert!(py.contains("def _unpack_Contact(")); - assert!(py.contains("contact_type=ContactType(_r.read_i32()),")); - - assert!(py.contains("def create_contact(")); - assert!(py.contains("first_name: str")); - assert!(py.contains("last_name: str")); - assert!(py.contains("email: Optional[str]")); - assert!(py.contains("contact_type: \"ContactType\"")); - assert!(py.contains("-> int:")); - assert!(py.contains("weaveffi_contacts_create_contact")); - assert!(py.contains("_string_to_bytes(first_name)")); - assert!(py.contains("contact_type.value")); - - assert!(py.contains("def get_contact(id: int) -> \"Contact\":")); - assert!(py.contains("weaveffi_contacts_get_contact")); - assert!(py.contains("return _unpack_Contact(_data)")); - - assert!(py.contains("def list_contacts() -> List[\"Contact\"]:")); - assert!(py.contains("weaveffi_contacts_list_contacts")); - assert!(py.contains("[_read_Contact(_r) for _i0 in range(_r.read_len())]")); - - assert!(py.contains("def delete_contact(id: int) -> bool:")); - assert!(py.contains("weaveffi_contacts_delete_contact")); - assert!(py.contains("return bool(_result)")); - - assert!(py.contains("def count_contacts() -> int:")); - assert!(py.contains("weaveffi_contacts_count_contacts")); - - assert!(pyi.contains("class ContactType(IntEnum):")); - assert!(pyi.contains(" Personal: int")); - assert!(pyi.contains(" Work: int")); - assert!(pyi.contains(" Other: int")); - assert!(pyi.contains("class Contact:")); - assert!(pyi.contains("def create_contact(")); - assert!(pyi.contains("def get_contact(id: int) -> \"Contact\": ...")); - assert!(pyi.contains("def list_contacts() -> List[\"Contact\"]: ...")); - assert!(pyi.contains("def delete_contact(id: int) -> bool: ...")); - assert!(pyi.contains("def count_contacts() -> int: ...")); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn python_generates_packaging() { - let api = make_api(vec![simple_module(vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }])]); - - let tmp = std::env::temp_dir().join("weaveffi_test_python_packaging"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - PythonGenerator - .generate(&api, out_dir, &PythonConfig::default()) - .unwrap(); - - let pyproject = std::fs::read_to_string(tmp.join("python/pyproject.toml")).unwrap(); - assert!( - pyproject.contains("[build-system]"), - "missing build-system: {pyproject}" - ); - assert!( - pyproject.contains("setuptools"), - "missing setuptools: {pyproject}" - ); - assert!( - pyproject.contains("[project]"), - "missing project section: {pyproject}" - ); - assert!( - pyproject.contains("name = \"weaveffi\""), - "missing project name: {pyproject}" - ); - assert!( - pyproject.contains("version = \"0.1.0\""), - "missing version: {pyproject}" - ); - assert!( - pyproject.contains("[tool.setuptools]"), - "missing tool.setuptools: {pyproject}" - ); - assert!( - pyproject.contains("packages = [\"weaveffi\"]"), - "missing packages list: {pyproject}" - ); - - let setup = std::fs::read_to_string(tmp.join("python/setup.py")).unwrap(); - assert!( - setup.contains("from setuptools import setup"), - "missing setuptools import: {setup}" - ); - assert!( - setup.contains("name=\"weaveffi\""), - "missing package name: {setup}" - ); - - let readme = std::fs::read_to_string(tmp.join("python/README.md")).unwrap(); - assert!( - readme.contains("pip install"), - "missing install instructions: {readme}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn python_has_memory_helpers() { - let api = make_api(vec![]); - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("import contextlib"), - "missing contextlib import" - ); - assert!( - py.contains("class _PointerGuard(contextlib.AbstractContextManager):"), - "missing _PointerGuard class" - ); - assert!( - py.contains("def __exit__(self, *exc)"), - "missing _PointerGuard.__exit__" - ); - assert!( - py.contains("def _string_to_bytes("), - "missing _string_to_bytes helper" - ); - assert!( - py.contains("def _bytes_to_string("), - "missing _bytes_to_string helper" - ); -} - -#[test] -fn python_custom_package_name() { - let api = make_api(vec![simple_module(vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }])]); - - let config = PythonConfig { - package_name: Some("my_bindings".into()), - ..PythonConfig::default() - }; - - let tmp = std::env::temp_dir().join("weaveffi_test_py_custom_pkg"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - PythonGenerator.generate(&api, out_dir, &config).unwrap(); - - assert!( - tmp.join("python/my_bindings/__init__.py").exists(), - "package dir should use custom name" - ); - assert!( - tmp.join("python/my_bindings/weaveffi.py").exists(), - "module file should be inside custom package dir" - ); - - let pyproject = std::fs::read_to_string(tmp.join("python/pyproject.toml")).unwrap(); - assert!( - pyproject.contains("name = \"my_bindings\""), - "pyproject.toml should use custom name: {pyproject}" - ); - assert!( - pyproject.contains("packages = [\"my_bindings\"]"), - "pyproject.toml packages should use custom name: {pyproject}" - ); - - let setup = std::fs::read_to_string(tmp.join("python/setup.py")).unwrap(); - assert!( - setup.contains("name=\"my_bindings\""), - "setup.py should use custom name: {setup}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn python_strip_module_prefix() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "create_contact".into(), - params: vec![Param { - name: "name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let config = PythonConfig { - strip_module_prefix: true, - ..PythonConfig::default() - }; - - let tmp = std::env::temp_dir().join("weaveffi_test_python_strip_prefix"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - PythonGenerator.generate(&api, out_dir, &config).unwrap(); - - let py = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.py")).unwrap(); - assert!( - py.contains("def create_contact("), - "stripped name should be create_contact: {py}" - ); - assert!( - !py.contains("def contacts_create_contact("), - "should not contain module-prefixed name: {py}" - ); - assert!( - py.contains("weaveffi_contacts_create_contact"), - "C ABI call should still use full name: {py}" - ); - - let pyi = std::fs::read_to_string(tmp.join("python/weaveffi/weaveffi.pyi")).unwrap(); - assert!( - pyi.contains("def create_contact("), - "pyi stripped name should be create_contact: {pyi}" - ); - - // Stripping is the default; module-prefixed names are opt-in. - assert!(PythonConfig::default().strip_module_prefix); - - let no_strip = PythonConfig { - strip_module_prefix: false, - ..PythonConfig::default() - }; - let tmp2 = std::env::temp_dir().join("weaveffi_test_python_no_strip_prefix"); - let _ = std::fs::remove_dir_all(&tmp2); - std::fs::create_dir_all(&tmp2).unwrap(); - let out_dir2 = Utf8Path::from_path(&tmp2).expect("valid UTF-8"); - - PythonGenerator.generate(&api, out_dir2, &no_strip).unwrap(); - - let py2 = std::fs::read_to_string(tmp2.join("python/weaveffi/weaveffi.py")).unwrap(); - assert!( - py2.contains("def contacts_create_contact("), - "opting out should use module-prefixed name: {py2}" - ); - - let pyi2 = std::fs::read_to_string(tmp2.join("python/weaveffi/weaveffi.pyi")).unwrap(); - assert!( - pyi2.contains("def contacts_create_contact("), - "pyi opt-out should use module-prefixed name: {pyi2}" - ); - - let _ = std::fs::remove_dir_all(&tmp); - let _ = std::fs::remove_dir_all(&tmp2); -} - -#[test] -fn python_deeply_nested_optional() { - let api = make_api(vec![Module { - name: "edge".into(), - functions: vec![Function { - name: "process".into(), - params: vec![Param { - name: "data".into(), - ty: TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional( - Box::new(TypeRef::Record("Contact".into())), - ))))), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let pyi = render_pyi_module(&BindingModel::build(&api, "weaveffi"), true, "weaveffi.yml"); - assert!( - pyi.contains("Optional[List[Optional["), - "should contain deeply nested optional type: {pyi}" - ); -} - -#[test] -fn python_map_of_lists() { - let api = make_api(vec![Module { - name: "edge".into(), - functions: vec![Function { - name: "process".into(), - params: vec![Param { - name: "scores".into(), - ty: TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::List(Box::new(TypeRef::I32))), - ), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let pyi = render_pyi_module(&BindingModel::build(&api, "weaveffi"), true, "weaveffi.yml"); - assert!( - pyi.contains("Dict[str, List[int]]"), - "should contain map of lists type: {pyi}" - ); -} - -#[test] -fn python_enum_keyed_map() { - let api = make_api(vec![Module { - name: "edge".into(), - functions: vec![Function { - name: "process".into(), - params: vec![Param { - name: "contacts".into(), - ty: TypeRef::Map( - Box::new(TypeRef::Enum("Color".into())), - Box::new(TypeRef::Record("Contact".into())), - ), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![EnumDef { - name: "Color".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Blue".into(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let pyi = render_pyi_module(&BindingModel::build(&api, "weaveffi"), true, "weaveffi.yml"); - assert!( - pyi.contains("Dict[\"Color\", \"Contact\"]"), - "should contain enum-keyed map type: {pyi}" - ); -} - -#[test] -fn python_typed_handle_type() { - let api = ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules: vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "get_info".into(), - params: vec![Param { - name: "contact".into(), - ty: TypeRef::TypedHandle("Contact".into()), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }); - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - // A typed handle is an opaque token: hinted as int, passed raw. - assert!( - py.contains("def get_info(contact: int) -> None:"), - "TypedHandle should hint as int: {py}" - ); - assert!( - !py.contains("contact._ptr"), - "TypedHandle call arg must pass the raw value: {py}" - ); - assert!( - py.contains("_fn(contact, ctypes.byref(_err))"), - "TypedHandle should pass through unchanged: {py}" - ); - assert!( - py.contains("ctypes.c_void_p"), - "TypedHandle ctypes type should be c_void_p: {py}" - ); -} - -#[test] -fn python_no_double_free_on_error() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "find_contact".into(), - params: vec![Param { - name: "name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - - assert!( - py.contains("_string_to_bytes(name)"), - "string param should use _string_to_bytes(name): {py}" - ); - assert!( - !py.contains("weaveffi_free_string(name"), - "input string param must not be freed with weaveffi_free_string(name): {py}" - ); - assert!( - !py.contains("free(name"), - "input string param must not be passed to free(name: {py}" - ); - - let fn_sig = "def find_contact(name: str) -> \"Contact\":"; - let start = py - .find(fn_sig) - .unwrap_or_else(|| panic!("missing find_contact signature: {py}")); - let rest = &py[start..]; - let end_offset = rest[1..] - .find("\n\ndef ") - .or_else(|| rest[1..].find("\n\nclass ")) - .map(|i| i + 1) - .unwrap_or(rest.len()); - let body = &rest[..end_offset]; - // The error check runs before the returned buffer is touched, so a - // trap never leaves a dangling decode. - let err_pos = body - .find("_check_error(_err)") - .expect("_check_error should appear in find_contact"); - let take_pos = body - .find("_data = _take_buffer(_result, _out_len.value)") - .expect("buffer take should appear in find_contact"); - let contact_pos = body - .find("return _unpack_Contact(_data)") - .expect("return _unpack_Contact(_data) should appear in find_contact"); - assert!( - err_pos < take_pos && take_pos < contact_pos, - "_check_error(_err) should precede the buffer take and decode: {body}" - ); - - // Records are value types now: no destructor exists to double-free. - let class_start = py - .find("class Contact:") - .expect("Contact class should be defined"); - let after_class = &py[class_start..]; - let class_end = after_class[1..] - .find("\n\nclass ") - .or_else(|| after_class[1..].find("\n\ndef ")) - .map(|i| i + 1) - .unwrap_or(after_class.len()); - let contact_class = &after_class[..class_end]; - assert!( - !contact_class.contains("def __del__(self)"), - "Contact must not define __del__: {contact_class}" - ); - assert!( - !contact_class.contains("_destroy"), - "Contact must not reference a destroy symbol: {contact_class}" - ); -} - -#[test] -fn python_null_check_on_optional_return() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "find_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Optional(Box::new(TypeRef::Record( - "Contact".into(), - )))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - - // An optional record is buffered: the None case rides the option - // flag inside the value buffer rather than a null pointer check. - assert!( - py.contains("_data = _take_buffer(_result, _out_len.value)"), - "optional struct return should take the buffer: {py}" - ); - assert!( - py.contains( - "return _decode_buffer(_data, \ - lambda _r: (_read_Contact(_r) if _r.read_option_flag() else None))" - ), - "optional struct return should decode through the flag: {py}" - ); -} - -#[test] -fn python_async_function_is_async_def() { - let api = make_api(vec![simple_module(vec![Function { - name: "fetch_data".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }])]); - let code = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - code.contains("import asyncio"), - "should import asyncio: {code}" - ); - assert!( - code.contains("async def fetch_data(id: int) -> str:"), - "should have async wrapper: {code}" - ); - // Callback-driven: the wrapper awaits a future resolved by the C - // completion callback, rather than blocking an executor thread. - assert!( - code.contains("_loop = asyncio.get_running_loop()"), - "should use get_running_loop: {code}" - ); - assert!( - code.contains("_fut = _loop.create_future()"), - "should create a future: {code}" - ); - // The string result slot is `c_void_p` (a `c_char_p` slot would - // auto-convert to `bytes` and lose the pointer `_take_string` frees). - assert!( - code.contains( - "_cb_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p, \ - ctypes.POINTER(_WeaveFFIErrorStruct), ctypes.c_void_p)" - ), - "should build the CFUNCTYPE trampoline: {code}" - ); - assert!( - code.contains("_loop.call_soon_threadsafe(_resolve)"), - "should resolve via call_soon_threadsafe: {code}" - ); - assert!( - code.contains("return await _fut"), - "should await the future: {code}" - ); - assert!( - !code.contains("run_in_executor"), - "executor-based async must be gone: {code}" - ); - assert!( - !code.contains("_fetch_data_sync"), - "sync helper must be gone: {code}" - ); -} - -#[test] -fn listener_register_unregister_wrappers() { - use weaveffi_ir::ir::{CallbackDef, ListenerDef}; - let api = make_api(vec![Module { - callbacks: vec![CallbackDef { - name: "OnMessage".into(), - params: vec![Param { - name: "message".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - doc: None, - }], - listeners: vec![ListenerDef { - name: "message_listener".into(), - event_callback: "OnMessage".into(), - doc: None, - }], - ..simple_module(vec![]) - }]); - let code = render_python_module(&api, false, "weaveffi", "weaveffi.yml"); - // CFUNCTYPE alias matches the C typedef shape: (const char*, void*). - assert!( - code.contains( - "_CFUNC_weaveffi_math_OnMessage_fn = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_void_p)" - ), - "callback CFUNCTYPE alias: {code}" - ); - // Registry pinning keeps the trampoline alive until unregister. - assert!( - code.contains("_listener_refs: Dict[int, object] = {}"), - "listener registry: {code}" - ); - assert!( - code.contains( - "def math_register_message_listener(callback: Callable[[str], None]) -> int:" - ), - "register wrapper: {code}" - ); - assert!( - code.contains("callback(_bytes_to_string(message))"), - "trampoline converts the C string: {code}" - ); - assert!( - code.contains("_listener_refs[_listener_id] = _cfunc"), - "register pins the trampoline: {code}" - ); - assert!( - code.contains("def math_unregister_message_listener(listener_id: int) -> None:"), - "unregister wrapper: {code}" - ); - assert!( - code.contains("_listener_refs.pop(listener_id, None)"), - "unregister releases the trampoline: {code}" - ); -} - -#[test] -fn listener_bytes_and_enum_params_convert() { - use weaveffi_ir::ir::{CallbackDef, EnumDef, EnumVariant, ListenerDef}; - let api = make_api(vec![Module { - enums: vec![EnumDef { - name: "Level".into(), - doc: None, - variants: vec![EnumVariant { - name: "Info".into(), - value: 0, - doc: None, - fields: vec![], - }], - }], - callbacks: vec![CallbackDef { - name: "OnChunk".into(), - params: vec![ - Param { - name: "data".into(), - ty: TypeRef::Bytes, - mutable: false, - doc: None, - }, - Param { - name: "level".into(), - ty: TypeRef::Enum("Level".into()), - mutable: false, - doc: None, - }, - ], - doc: None, - }], - listeners: vec![ListenerDef { - name: "chunks".into(), - event_callback: "OnChunk".into(), - doc: None, - }], - ..simple_module(vec![]) - }]); - let code = render_python_module(&api, false, "weaveffi", "weaveffi.yml"); - // Bytes lower to (ptr, len) slots; the trampoline reconstructs bytes. - assert!( - code.contains("def _trampoline(data_ptr, data_len, level, _context):"), - "trampoline signature has flattened slots: {code}" - ); - assert!( - code.contains("bytes(data_ptr[:data_len]) if data_ptr else b\"\""), - "bytes param converts: {code}" - ); - assert!( - code.contains("Level(level)"), - "enum param converts to IntEnum: {code}" - ); -} - -/// `ctypes.CFUNCTYPE` instances pin the C trampoline. Because the wrapper -/// suspends at `await` and its frame can be torn down by cancellation, the -/// trampoline is registered in the module-level `_async_pending` dict -/// under an integer token, and the completion callback pops that entry -/// before resolving the future (skipping a cancelled future). -#[test] -fn python_async_pins_callback_for_lifetime() { - let api = make_api(vec![simple_module(vec![Function { - name: "fetch_data".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }])]); - let code = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - let pin_count = code.matches("_cb = _cb_type(_cb_impl)").count(); - assert_eq!( - pin_count, 1, - "expected one `_cb = _cb_type(_cb_impl)` per async fn, got {pin_count}: {code}" - ); - // The module-level registry and its helper are emitted once. - assert!( - code.contains("_async_pending: Dict[int, object] = {}"), - "missing pending-trampoline registry: {code}" - ); - assert!( - code.contains("def _async_register(cb) -> int:"), - "missing _async_register helper: {code}" - ); - // Every registration is matched by a pop on completion, and a - // cancelled future is left untouched. - let register_count = code.matches("_token = _async_register(_cb)").count(); - let pop_count = code.matches("_async_pending.pop(_token, None)").count(); - assert_eq!(register_count, 1, "expected one registration: {code}"); - assert_eq!( - register_count, pop_count, - "every registration must be popped on completion: {code}" - ); - assert!( - code.contains("if _fut.cancelled():"), - "missing cancelled-future guard: {code}" - ); -} - -#[test] -fn python_pyi_async_function() { - let api = make_api(vec![simple_module(vec![Function { - name: "fetch_data".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }])]); - let stubs = render_pyi_module(&BindingModel::build(&api, "weaveffi"), true, "weaveffi.yml"); - assert!( - stubs.contains("async def fetch_data(id: int) -> str: ..."), - "pyi should declare async def: {stubs}" - ); -} - -#[test] -fn python_cross_module_struct() { - let api = make_api(vec![ - Module { - name: "types".into(), - functions: vec![], - structs: vec![StructDef { - name: "Name".into(), - doc: None, - fields: vec![StructField { - name: "value".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }, - Module { - name: "ops".into(), - functions: vec![Function { - name: "get_name".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("types.Name".into())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }, - ]); - - let code = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - let stubs = render_pyi_module(&BindingModel::build(&api, "weaveffi"), true, "weaveffi.yml"); - - assert!( - code.contains("return _unpack_Name(_data)"), - "cross-module return should decode via _unpack_Name: {code}" - ); - assert!( - !code.contains("types.Name"), - "dot-qualified name should not appear in generated Python code: {code}" - ); - assert!( - stubs.contains("\"Name\""), - "pyi should use local type name: {stubs}" - ); - assert!( - !stubs.contains("types.Name"), - "dot-qualified name should not appear in pyi stubs: {stubs}" - ); -} - -#[test] -fn python_nested_module_output() { - let api = make_api(vec![Module { - name: "parent".to_string(), - functions: vec![Function { - name: "outer_fn".to_string(), - params: vec![], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![Module { - name: "child".to_string(), - functions: vec![Function { - name: "inner_fn".to_string(), - params: vec![], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }], - }]); - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("# === Module: parent ==="), - "parent module section missing: {py}" - ); - assert!( - py.contains("# === Module: parent_child ==="), - "nested child module section missing: {py}" - ); - assert!( - py.contains("weaveffi_parent_outer_fn"), - "parent C function missing: {py}" - ); - assert!( - py.contains("weaveffi_parent_child_inner_fn"), - "nested child C function missing: {py}" - ); - let pyi = render_pyi_module(&BindingModel::build(&api, "weaveffi"), true, "weaveffi.yml"); - assert!( - pyi.contains("def inner_fn"), - "nested child function missing from pyi: {pyi}" - ); -} - -#[test] -fn python_type_hint_iterator() { - assert_eq!( - py_type_hint(&TypeRef::Iterator(Box::new(TypeRef::I32))), - "Iterator[int]" - ); - assert_eq!( - py_type_hint(&TypeRef::Iterator(Box::new(TypeRef::Record( - "Contact".into() - )))), - "Iterator[\"Contact\"]" - ); -} - -#[test] -fn python_iterator_return() { - let api = make_api(vec![Module { - name: "data".to_string(), - functions: vec![Function { - name: "list_items".to_string(), - params: vec![], - returns: Some(TypeRef::Iterator(Box::new(TypeRef::I32))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("class _ListItemsIterator:"), - "should emit iterator helper class: {py}" - ); - // Lazy contract: the wrapper hands back the helper instance; nothing - // drains the stream into a list. - assert!( - py.contains("def list_items() -> Iterator[int]:"), - "wrapper should be typed Iterator[int]: {py}" - ); - assert!( - py.contains("return _ListItemsIterator(_result)"), - "wrapper should return the iterator instance: {py}" - ); - assert!( - !py.contains("_items = []"), - "eager draining must be gone: {py}" - ); - // One producer pull per step, and disposal is single-shot via - // exhaustion, close(), or garbage collection. - assert!(py.contains("def __next__(self):"), "missing __next__: {py}"); - assert!( - py.contains("_next_fn = _lib.weaveffi_data_ListItemsIterator_next"), - "missing per-step next call: {py}" - ); - assert!(py.contains("def close(self):"), "missing close(): {py}"); - assert!(py.contains("def __del__(self):"), "missing __del__: {py}"); - assert!( - py.contains("_destroy_fn = _lib.weaveffi_data_ListItemsIterator_destroy"), - "missing destroy call: {py}" - ); -} - -#[test] -fn python_iterator_string_elements_freed() { - let api = make_api(vec![Module { - name: "data".to_string(), - functions: vec![Function { - name: "list_names".to_string(), - params: vec![], - returns: Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - // The out slot is a raw address so the pointer survives to be freed; - // each yielded string is copied and released via `_take_string`. - assert!( - py.contains("_out_item = ctypes.c_void_p()"), - "string out slot must be c_void_p: {py}" - ); - assert!( - py.contains("return _take_string(_out_item.value)"), - "yielded string must be copied then freed: {py}" - ); -} - -#[test] -fn deprecated_function_generates_annotation() { - let api = make_api(vec![simple_module(vec![Function { - name: "add_old".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: Some("Use add_v2 instead".into()), - since: Some("0.1.0".into()), - }])]); - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("warnings.warn(\"Use add_v2 instead\", DeprecationWarning, stacklevel=2)"), - "missing deprecation warning: {py}" - ); -} - -fn doc_api() -> ResolvedApi { - make_api(vec![Module { - name: "docs".into(), - functions: vec![Function { - name: "do_thing".into(), - params: vec![Param { - name: "x".into(), - ty: TypeRef::I32, - mutable: false, - doc: Some("the input value".into()), - }], - returns: Some(TypeRef::I32), - doc: Some("Performs a thing.".into()), - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Item".into(), - doc: Some("An item we track.".into()), - fields: vec![StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: Some("Stable id".into()), - }], - }], - enums: vec![EnumDef { - name: "Kind".into(), - doc: Some("Kind of item.".into()), - variants: vec![EnumVariant { - name: "Small".into(), - value: 0, - doc: Some("A small one".into()), - fields: vec![], - }], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]) -} - -#[test] -fn python_emits_doc_on_function() { - let py = render_python_module(&doc_api(), true, "weaveffi", "weaveffi.yml"); - assert!(py.contains("\"\"\"Performs a thing."), "{py}"); -} - -#[test] -fn python_emits_doc_on_struct() { - let py = render_python_module(&doc_api(), true, "weaveffi", "weaveffi.yml"); - assert!(py.contains("\"\"\"An item we track.\"\"\""), "{py}"); -} - -#[test] -fn python_emits_doc_on_enum_variant() { - let py = render_python_module(&doc_api(), true, "weaveffi", "weaveffi.yml"); - assert!(py.contains("\"\"\"Kind of item.\"\"\""), "{py}"); - assert!(py.contains("# A small one"), "{py}"); -} - -#[test] -fn python_emits_doc_on_field() { - let py = render_python_module(&doc_api(), true, "weaveffi", "weaveffi.yml"); - // Field docs surface as a comment above the dataclass field. - assert!(py.contains("# Stable id"), "{py}"); -} - -#[test] -fn python_emits_doc_on_param() { - let py = render_python_module(&doc_api(), true, "weaveffi", "weaveffi.yml"); - assert!(py.contains("Parameters"), "{py}"); - assert!(py.contains("x : the input value"), "{py}"); -} - -#[test] -fn python_custom_prefix_threads_to_user_symbols() { - let api = make_api(vec![simple_module(vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }])]); - - let py = render_python_module(&api, true, "myffi", "weaveffi.yml"); - - // User symbols honor the configured ABI prefix. - assert!( - py.contains("_lib.myffi_math_add"), - "user symbol should use the custom prefix: {py}" - ); - assert!( - !py.contains("weaveffi_math_add"), - "user symbol must not hard-code the weaveffi_ prefix: {py}" - ); - - // Runtime ABI helpers stay literal regardless of the user prefix. - assert!( - py.contains("weaveffi_error_clear"), - "runtime ABI helper must remain literal: {py}" - ); - assert!( - !py.contains("myffi_error_clear"), - "runtime ABI helper must not be prefixed: {py}" - ); -} - -/// A `kv` module declaring a `KvError` domain, a throwing and a -/// non-throwing free function, and a `Store` interface exercising the -/// canonical `new` constructor, a factory constructor, an instance method -/// with a string parameter and return, and a static. -fn kv_api() -> ResolvedApi { - use weaveffi_ir::ir::{ErrorCode, ErrorDomain, InterfaceDef}; - - let fn_lit = - |name: &str, params: Vec, returns: Option, throws: bool| Function { - name: name.into(), - params, - returns, - doc: None, - throws, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }; - let str_param = |name: &str| Param { - name: name.into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }; - - make_api(vec![Module { - name: "kv".into(), - functions: vec![ - fn_lit( - "lookup", - vec![str_param("key")], - Some(TypeRef::StringUtf8), - true, - ), - fn_lit("reset", vec![], None, false), - ], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![InterfaceDef { - name: "Store".into(), - doc: Some("A key-value store handle.".into()), - constructors: vec![ - fn_lit("new", vec![str_param("path")], None, true), - fn_lit("open_readonly", vec![str_param("path")], None, true), - ], - methods: vec![fn_lit( - "get", - vec![str_param("key")], - Some(TypeRef::StringUtf8), - true, - )], - statics: vec![fn_lit("version", vec![], Some(TypeRef::StringUtf8), false)], - }], - errors: Some(ErrorDomain { - name: "KvError".into(), - codes: vec![ - ErrorCode { - name: "KEY_NOT_FOUND".into(), - code: 1, - message: "key not found".into(), - doc: Some("Raised when the key is absent.".into()), - fields: vec![], - }, - ErrorCode { - name: "IO_FAILURE".into(), - code: 2, - message: "io failure".into(), - doc: None, - fields: vec![], - }, - ], - }), - modules: vec![], - }]) -} - -#[test] -fn python_interface_async_and_iterator_members() { - use weaveffi_ir::ir::InterfaceDef; - let mut api = kv_api().api().clone(); - api.modules[0].interfaces = vec![InterfaceDef { - name: "Store".into(), - doc: None, - constructors: vec![Function { - name: "new".into(), - params: vec![], - returns: None, - doc: None, - throws: true, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - methods: vec![ - Function { - name: "fetch".into(), - params: vec![Param { - name: "key".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - throws: true, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "list_keys".into(), - params: vec![], - returns: Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - doc: None, - throws: true, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - statics: vec![Function { - name: "default_path".into(), - params: vec![], - returns: Some(TypeRef::StringUtf8), - doc: None, - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - }]; - let api = ResolvedApi::assume_resolved(api); - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - - // Async method: the wrapper itself is the async def, and the launcher - // receives self._ptr, the marshalled params, the trampoline, and the - // NULL context. - assert!( - py.contains("import asyncio"), - "missing asyncio import: {py}" - ); - assert!( - py.contains("async def fetch(self, key: str) -> str:"), - "missing async def method: {py}" - ); - assert!( - py.contains("_fn(self._ptr, _string_to_bytes(key), _cb, None)"), - "async launcher should receive self._ptr first: {py}" - ); - assert!( - !py.contains("run_in_executor"), - "executor-based async must be gone: {py}" - ); - // A throwing async member maps errors through the domain factory, - // handing over the copied payload buffer. - assert!( - py.contains("_state[\"err\"] = _kv_error_from(_code, _msg, _payload)"), - "throwing async member should build domain errors: {py}" - ); - - // Iterator method: the helper class is emitted at module scope, - // qualified by the interface name, and the wrapper hands it back - // without draining. - assert!( - py.contains("class _StoreListKeysIterator:"), - "missing interface-qualified iterator helper: {py}" - ); - assert!( - py.contains("def list_keys(self) -> Iterator[str]:"), - "missing iterator method: {py}" - ); - assert!( - py.contains("return _StoreListKeysIterator(_result)"), - "iterator method should return the helper instance: {py}" - ); - // Per-next errors route through the domain checker for a throwing - // member. - assert!( - py.contains("_check_kv_error(_err)"), - "iterator next should use the domain checker: {py}" - ); - - // Async static: also callback-driven; a non-throwing member falls - // back to the generic error. - assert!( - py.contains("async def default_path() -> str:"), - "missing async static wrapper: {py}" - ); - assert!( - py.contains("_state[\"err\"] = WeaveFFIError(_code, _msg)"), - "non-throwing async member keeps the generic error: {py}" - ); -} - -#[test] -fn python_typed_error_domain_classes() { - let py = render_python_module(&kv_api(), true, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("class KvError(WeaveFFIError):"), - "missing domain base class: {py}" - ); - assert!( - py.contains("class KeyNotFound(KvError):"), - "missing per-code subclass: {py}" - ); - assert!(py.contains("CODE = 1"), "missing CODE attr: {py}"); - assert!( - py.contains("class IoFailure(KvError):"), - "missing second per-code subclass: {py}" - ); - assert!( - py.contains("\"\"\"Raised when the key is absent.\"\"\""), - "per-code class should carry its doc: {py}" - ); - assert!( - py.contains("\"\"\"io failure\"\"\""), - "per-code class should fall back to its message: {py}" - ); - assert!( - py.contains("def __init__(self, message: str = \"key not found\") -> None:"), - "per-code class should default its message: {py}" - ); - assert!( - py.contains("KvError.KeyNotFound = KeyNotFound"), - "code classes should attach to the domain for scoped catches: {py}" - ); - assert!( - py.contains("1: KeyNotFound,"), - "missing code table entry: {py}" - ); - assert!( - py.contains( - "def _kv_error_from(code: int, message: str, payload: bytes = b\"\") \ - -> WeaveFFIError:" - ), - "missing factory: {py}" - ); - assert!( - py.contains("def _check_kv_error(err: _WeaveFFIErrorStruct) -> None:"), - "missing domain checker: {py}" - ); - assert!( - py.contains("raise _kv_error_from(code, message, payload)"), - "checker should raise through the factory: {py}" - ); -} - -#[test] -fn python_throwing_fn_uses_domain_checker() { - let py = render_python_module(&kv_api(), true, "weaveffi", "weaveffi.yml"); - let lookup = py - .split("def lookup(") - .nth(1) - .expect("lookup wrapper present"); - let lookup_body = lookup.split("\n\n").next().unwrap(); - assert!( - lookup_body.contains("_check_kv_error(_err)"), - "throwing fn should route through the domain checker: {py}" - ); -} - -#[test] -fn python_non_throwing_fn_uses_generic_checker() { - let py = render_python_module(&kv_api(), true, "weaveffi", "weaveffi.yml"); - let reset = py - .split("def reset(") - .nth(1) - .expect("reset wrapper present"); - let reset_body = reset.split("\n\n").next().unwrap(); - assert!( - reset_body.contains("def reset() -> None:") - || reset.starts_with(") -> None:") - || py.contains("def reset() -> None:"), - "non-throwing fn keeps a plain signature: {py}" - ); - assert!( - reset_body.contains("_check_error(_err)"), - "non-throwing fn should use the generic checker: {py}" - ); - assert!( - !reset_body.contains("_check_kv_error"), - "non-throwing fn must not use the domain checker: {py}" - ); -} - -#[test] -fn python_interface_class_generated() { - let py = render_python_module(&kv_api(), true, "weaveffi", "weaveffi.yml"); - - assert!(py.contains("class Store:"), "missing wrapper class: {py}"); - assert!( - py.contains("\"\"\"A key-value store handle.\"\"\""), - "missing interface docstring: {py}" - ); - assert!( - py.contains("def _from_ptr(cls, ptr) -> \"Store\":"), - "missing _from_ptr wrapper hook: {py}" - ); - - // `new` becomes `__init__`, calling the constructor symbol and - // stashing the owned pointer. - assert!( - py.contains("def __init__(self, path: str) -> None:"), - "missing __init__ from ctor `new`: {py}" - ); - assert!( - py.contains("_lib.weaveffi_kv_Store_new"), - "missing ctor symbol: {py}" - ); - assert!( - py.contains("self._ptr = _result"), - "__init__ should own the returned pointer: {py}" - ); - - // The second constructor is a classmethod factory. - assert!( - py.contains("@classmethod\n def open_readonly(cls, path: str) -> \"Store\":"), - "missing classmethod factory: {py}" - ); - assert!( - py.contains("return cls._from_ptr(_result)"), - "factory should wrap via _from_ptr: {py}" - ); - - // Instance method: string param and return, `self._ptr` leading arg. - assert!( - py.contains("def get(self, key: str) -> str:"), - "missing method signature: {py}" - ); - assert!( - py.contains("_fn(self._ptr, _string_to_bytes(key), ctypes.byref(_err))"), - "method should pass self._ptr as the leading C argument: {py}" - ); - let get_body = py.split("def get(").nth(1).unwrap(); - let get_body = get_body.split("\n\n").next().unwrap(); - assert!( - get_body.contains("_check_kv_error(_err)"), - "throwing method should use the domain checker: {py}" - ); - - // Static member. - assert!( - py.contains("@staticmethod\n def version() -> str:"), - "missing staticmethod: {py}" - ); - assert!( - py.contains("_lib.weaveffi_kv_Store_version"), - "missing static symbol: {py}" - ); - - // Destroy wiring in __del__. - assert!( - py.contains("_lib.weaveffi_kv_Store_destroy(self._ptr)"), - "missing destroy call in __del__: {py}" - ); -} - -#[test] -fn python_interface_param_and_return_marshalling() { - use weaveffi_ir::ir::InterfaceDef; - let api = make_api(vec![Module { - name: "kv".into(), - functions: vec![ - Function { - name: "clone_store".into(), - params: vec![Param { - name: "store".into(), - ty: TypeRef::Interface("Store".into()), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Interface("Store".into())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "find_store".into(), - params: vec![], - returns: Some(TypeRef::Optional(Box::new(TypeRef::Interface( - "Store".into(), - )))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![InterfaceDef { - name: "Store".into(), - doc: None, - constructors: vec![], - methods: vec![], - statics: vec![], - }], - errors: None, - modules: vec![], - }]); - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - - // An interface parameter is borrowed: the wrapper passes its pointer. - assert!( - py.contains("def clone_store(store: \"Store\") -> \"Store\":"), - "missing interface hints: {py}" - ); - assert!( - py.contains("_fn(store._ptr, ctypes.byref(_err))"), - "interface param should pass ._ptr: {py}" - ); - // A returned interface wraps the owned pointer via _from_ptr. - assert!( - py.contains("return Store._from_ptr(_result)"), - "interface return should wrap via _from_ptr: {py}" - ); - // An optional interface return maps null to None. - let find = py.split("def find_store(").nth(1).unwrap(); - assert!( - find.contains("return None") && find.contains("Store._from_ptr(_result)"), - "optional interface return should null-check: {py}" - ); -} - -#[test] -fn python_naming_default_stripped_snake_case() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "createContact".into(), - params: vec![Param { - name: "firstName".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let config = PythonConfig::default(); - assert!(config.strip_module_prefix, "stripping must be the default"); - let py = render_python_module(&api, config.strip_module_prefix, "weaveffi", "weaveffi.yml"); - assert!( - py.contains("def create_contact(first_name: str) -> int:"), - "default naming should be bare snake_case incl. params: {py}" - ); - assert!( - py.contains("_fn(_string_to_bytes(first_name), ctypes.byref(_err))"), - "body references should use the snake_case param name: {py}" - ); - assert!( - !py.contains("def contacts_create_contact("), - "default should not module-prefix: {py}" - ); -} - -#[test] -fn python_throws_docstring_has_raises_section() { - let py = render_python_module(&kv_api(), true, "weaveffi", "weaveffi.yml"); - let lookup = py - .split("def lookup(") - .nth(1) - .expect("lookup wrapper present"); - assert!( - lookup.contains("Raises\n ------\n KvError\n"), - "throwing fn should document Raises: {py}" - ); - let reset = py - .split("def reset(") - .nth(1) - .expect("reset wrapper present"); - let reset_body = reset.split("\n\n").next().unwrap(); - assert!( - !reset_body.contains("Raises"), - "non-throwing fn must not document domain raises: {py}" - ); -} - -#[test] -fn python_pyi_errors_and_interfaces() { - let pyi = render_pyi_module( - &BindingModel::build(&kv_api(), "weaveffi"), - true, - "weaveffi.yml", - ); - assert!( - pyi.contains("class WeaveFFIError(Exception):"), - "stub should declare the generic error: {pyi}" - ); - assert!( - pyi.contains("class KvError(WeaveFFIError):"), - "stub should declare the domain base: {pyi}" - ); - assert!( - pyi.contains("class KeyNotFound(KvError):"), - "stub should declare per-code classes: {pyi}" - ); - assert!( - pyi.contains(" KeyNotFound: Type[\"KeyNotFound\"]"), - "stub should declare the scoped alias on the domain: {pyi}" - ); - assert!( - pyi.contains("class Store:"), - "stub should declare the interface class: {pyi}" - ); - assert!( - pyi.contains("def __init__(self, path: str) -> None: ..."), - "stub should declare __init__ for ctor `new`: {pyi}" - ); - assert!( - pyi.contains("def open_readonly(cls, path: str) -> \"Store\": ..."), - "stub should declare factory classmethods: {pyi}" - ); - assert!( - pyi.contains("def get(self, key: str) -> str: ..."), - "stub should declare methods: {pyi}" - ); - assert!( - pyi.contains("def version() -> str: ..."), - "stub should declare statics: {pyi}" - ); -} - -#[test] -fn preamble_includes_buffer_runtime() { - let api = make_api(vec![simple_module(vec![Function { - name: "noop".into(), - params: vec![], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }])]); - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - // The wire-format codec runtime ships in every generated module. - assert!(py.contains("import struct"), "missing struct import: {py}"); - assert!( - py.contains("class _BufferWriter:"), - "missing buffer writer: {py}" - ); - assert!( - py.contains("class _BufferReader:"), - "missing buffer reader: {py}" - ); - assert!( - py.contains("def _decode_buffer(data: bytes, read_fn):"), - "missing decode helper: {py}" - ); - assert!( - py.contains("def _take_buffer(ptr, length) -> bytes:"), - "missing take helper: {py}" - ); - // Little-endian packed encodings. - assert!( - py.contains("struct.pack(\" \"Shape.Tag\":"), - "missing tag property: {py}" - ); - - // One dataclass subclass per variant, with scoped aliases. - assert!( - py.contains("@dataclass\nclass ShapeCircle(Shape):"), - "missing Circle variant dataclass: {py}" - ); - assert!( - py.contains("TAG = Shape.Tag.Circle"), - "missing Circle TAG: {py}" - ); - assert!(py.contains("radius: float"), "missing Circle field: {py}"); - assert!( - py.contains("@dataclass\nclass ShapeRect(Shape):"), - "missing Rect variant dataclass: {py}" - ); - assert!( - py.contains("Shape.Circle = ShapeCircle"), - "missing scoped alias: {py}" - ); - - // No FFI symbols exist for rich enums. - assert!( - !py.contains("Shape_new_") && !py.contains("Shape_tag") && !py.contains("Shape_destroy"), - "rich enums must not call C symbols: {py}" - ); - - // The codec dispatches on isinstance and the i32 wire tag. - assert!( - py.contains("def _write_Shape(_w: _BufferWriter, value: \"Shape\") -> None:"), - "missing writer: {py}" - ); - assert!( - py.contains("if isinstance(value, ShapeCircle):"), - "missing isinstance dispatch: {py}" - ); - assert!( - py.contains("_w.write_f64(value.radius)"), - "missing variant field write: {py}" - ); - assert!( - py.contains("def _read_Shape(_r: _BufferReader) -> \"Shape\":"), - "missing reader: {py}" - ); - assert!( - py.contains("_tag = _r.read_i32()"), - "missing tag read: {py}" - ); - assert!( - py.contains("radius=_r.read_f64(),"), - "missing variant field read: {py}" - ); - assert!( - py.contains("unknown Shape tag"), - "missing unknown-tag rejection: {py}" - ); - - // The rich enum parameter packs like any buffered value. - assert!( - py.contains("_shape_buf = _pack_Shape(shape)"), - "missing param pack: {py}" - ); - assert!( - py.contains("_fn(_shape_buf, len(_shape_buf), ctypes.byref(_err))"), - "missing buffered call args: {py}" - ); -} - -#[test] -fn error_payload_decoding() { - use weaveffi_ir::ir::{ErrorCode, ErrorDomain}; - let api = make_api(vec![Module { - name: "kv".into(), - functions: vec![Function { - name: "get".into(), - params: vec![Param { - name: "key".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - throws: true, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: Some(ErrorDomain { - name: "KvError".into(), - codes: vec![ - ErrorCode { - name: "KEY_NOT_FOUND".into(), - code: 1, - message: "key not found".into(), - doc: None, - fields: vec![ - StructField { - name: "key".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "attempts".into(), - ty: TypeRef::I32, - doc: None, - }, - ], - }, - ErrorCode { - name: "IO_FAILURE".into(), - code: 2, - message: "io failure".into(), - doc: None, - fields: vec![], - }, - ], - }), - modules: vec![], - }]); - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - - // The payload decoder reads the code's fields in declaration order - // and attaches them as exception attributes. - assert!( - py.contains( - "def _kv_error_payload_key_not_found(_exc: WeaveFFIError, \ - _r: _BufferReader) -> None:" - ), - "missing payload decoder: {py}" - ); - assert!( - py.contains("_exc.key = _r.read_string()"), - "missing key attribute decode: {py}" - ); - assert!( - py.contains("_exc.attempts = _r.read_i32()"), - "missing attempts attribute decode: {py}" - ); - // Only codes with fields enter the payload table. - assert!( - py.contains("_KV_ERROR_PAYLOADS: Dict[int, Callable] = {"), - "missing payload table: {py}" - ); - assert!( - py.contains("1: _kv_error_payload_key_not_found,"), - "missing payload table entry: {py}" - ); - assert!( - !py.contains("_kv_error_payload_io_failure"), - "field-less code must not get a decoder: {py}" - ); - // The factory decodes the payload; the checker copies it before - // weaveffi_error_clear frees it. - assert!( - py.contains("_decoder = _KV_ERROR_PAYLOADS.get(code)"), - "factory should look up the decoder: {py}" - ); - assert!( - py.contains( - "payload = ctypes.string_at(err.payload_ptr, err.payload_len) \ - if err.payload_ptr else b\"\"" - ), - "checker should copy the payload before clearing: {py}" - ); - assert!( - py.contains("raise _kv_error_from(code, message, payload)"), - "checker should raise through the factory with the payload: {py}" - ); - // The stub declares the payload attributes on the code class. - let pyi = render_pyi_module(&BindingModel::build(&api, "weaveffi"), true, "weaveffi.yml"); - assert!( - pyi.contains("class KeyNotFound(KvError):"), - "stub should declare the code class: {pyi}" - ); - assert!( - pyi.contains(" key: str\n") && pyi.contains(" attempts: int\n"), - "stub should declare payload attributes: {pyi}" - ); -} - -#[test] -fn listener_buffered_record_param_decodes() { - use weaveffi_ir::ir::{CallbackDef, ListenerDef}; - let api = make_api(vec![Module { - name: "events".into(), - functions: vec![], - structs: vec![StructDef { - name: "Event".into(), - doc: None, - fields: vec![StructField { - name: "kind".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![CallbackDef { - name: "OnEvent".into(), - params: vec![Param { - name: "event".into(), - ty: TypeRef::Record("Event".into()), - mutable: false, - doc: None, - }], - doc: None, - }], - listeners: vec![ListenerDef { - name: "event_feed".into(), - event_callback: "OnEvent".into(), - doc: None, - }], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - - // The borrowed (ptr, len) pair is copied and decoded before the - // user callable runs; nothing is freed on the consumer side. - assert!( - py.contains( - "_unpack_Event(ctypes.string_at(event_ptr, event_len) \ - if event_ptr else b\"\")" - ), - "listener trampoline should decode the borrowed record: {py}" - ); - assert!( - py.contains("ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t"), - "trampoline CFUNCTYPE should carry the (ptr, len) slots: {py}" - ); -} - -#[test] -fn async_function_returns_buffered_record() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "fetch_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I64, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - - assert!( - py.contains("async def fetch_contact(id: int) -> \"Contact\":"), - "missing async wrapper: {py}" - ); - // The owned result buffer is copied and freed by `_take_buffer`, then - // decoded inside the completion trampoline. - assert!( - py.contains( - "_state[\"val\"] = _unpack_Contact(\ - _take_buffer(ctypes.cast(result_ptr, ctypes.c_void_p).value, result_len))" - ), - "trampoline should decode the owned result buffer: {py}" - ); - // Decode failures surface through the future rather than escaping - // the C callback. - assert!( - py.contains("except Exception as _exc:"), - "decode errors should be trapped: {py}" - ); - assert!( - py.contains("_state[\"err\"] = _exc"), - "trapped decode errors should resolve the future: {py}" - ); -} - -#[test] -fn iterator_buffered_record_elements() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "iter_contacts".into(), - params: vec![], - returns: Some(TypeRef::Iterator(Box::new(TypeRef::Record( - "Contact".into(), - )))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let py = render_python_module(&api, true, "weaveffi", "weaveffi.yml"); - - assert!( - py.contains("def iter_contacts() -> Iterator[\"Contact\"]:"), - "missing iterator wrapper: {py}" - ); - // The `_next` signature carries the encoded item pointer plus its - // trailing out_len. - assert!( - py.contains( - "_next_fn.argtypes = [ctypes.c_void_p, \ - ctypes.POINTER(ctypes.c_void_p), \ - ctypes.POINTER(ctypes.c_size_t), \ - ctypes.POINTER(_WeaveFFIErrorStruct)]" - ), - "next should take out_item and out_len: {py}" - ); - // Each element is copied, freed with weaveffi_free_bytes (via - // _take_buffer), then decoded. - assert!( - py.contains("return _unpack_Contact(_take_buffer(_out_item.value, _out_len.value))"), - "element should be taken then decoded: {py}" - ); -} - -/// A kitchen-sink API touching every buffered surface at once (record and -/// rich enum params and returns, nested optional/list/map fields, error -/// payloads, a buffered listener, an async buffered method, and a record -/// iterator) generates a complete package without panicking. -#[test] -fn kitchen_sink_api_generates() { - use weaveffi_ir::ir::{CallbackDef, ErrorCode, ErrorDomain, InterfaceDef, ListenerDef}; - let api = make_api(vec![Module { - name: "kitchen".into(), - functions: vec![ - Function { - name: "save".into(), - params: vec![ - Param { - name: "item".into(), - ty: TypeRef::Record("Item".into()), - mutable: false, - doc: None, - }, - Param { - name: "shape".into(), - ty: TypeRef::RichEnum("Shape".into()), - mutable: false, - doc: None, - }, - Param { - name: "tags".into(), - ty: TypeRef::List(Box::new(TypeRef::StringUtf8)), - mutable: false, - doc: None, - }, - Param { - name: "scores".into(), - ty: TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }, - Param { - name: "note".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::Record("Item".into())), - doc: None, - throws: true, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "fetch".into(), - params: vec![], - returns: Some(TypeRef::RichEnum("Shape".into())), - doc: None, - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "stream".into(), - params: vec![], - returns: Some(TypeRef::Iterator(Box::new(TypeRef::Record("Item".into())))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - structs: vec![StructDef { - name: "Item".into(), - doc: None, - fields: vec![ - StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "quantity".into(), - ty: TypeRef::U32, - doc: None, - }, - StructField { - name: "data".into(), - ty: TypeRef::Bytes, - doc: None, - }, - StructField { - name: "nested".into(), - ty: TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::F64), - ))))), - doc: None, - }, - ], - }], - enums: vec![EnumDef { - name: "Shape".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Dot".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Circle".into(), - value: 1, - doc: None, - fields: vec![StructField { - name: "radius".into(), - ty: TypeRef::F64, - doc: None, - }], - }, - ], - }], - callbacks: vec![CallbackDef { - name: "OnItem".into(), - params: vec![Param { - name: "item".into(), - ty: TypeRef::Record("Item".into()), - mutable: false, - doc: None, - }], - doc: None, - }], - listeners: vec![ListenerDef { - name: "item_feed".into(), - event_callback: "OnItem".into(), - doc: None, - }], - interfaces: vec![InterfaceDef { - name: "Store".into(), - doc: None, - constructors: vec![Function { - name: "new".into(), - params: vec![], - returns: None, - doc: None, - throws: true, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - methods: vec![Function { - name: "put".into(), - params: vec![Param { - name: "item".into(), - ty: TypeRef::Record("Item".into()), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Optional(Box::new(TypeRef::Record("Item".into())))), - doc: None, - throws: true, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - statics: vec![], - }], - errors: Some(ErrorDomain { - name: "KitchenError".into(), - codes: vec![ErrorCode { - name: "BAD_ITEM".into(), - code: 1, - message: "bad item".into(), - doc: None, - fields: vec![StructField { - name: "reason".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - }), - modules: vec![], - }]); - let dir = tempfile::tempdir().unwrap(); - let out = Utf8Path::from_path(dir.path()).unwrap(); - PythonGenerator - .generate(&api, out, &PythonConfig::default()) - .unwrap(); - let py = std::fs::read_to_string(out.join("python/weaveffi/weaveffi.py")).unwrap(); - assert!(py.contains("def _pack_Item("), "missing Item codec: {py}"); - assert!(py.contains("def _read_Shape("), "missing Shape codec: {py}"); - assert!( - py.contains("_kitchen_error_payload_bad_item"), - "missing payload decoder: {py}" - ); - assert!( - std::fs::metadata(out.join("python/weaveffi/weaveffi.pyi")).is_ok(), - "missing stub file" - ); -} diff --git a/crates/weaveffi-gen-python/src/types.rs b/crates/weaveffi-gen-python/src/types.rs index 35f88b00..c8c8a386 100644 --- a/crates/weaveffi-gen-python/src/types.rs +++ b/crates/weaveffi-gen-python/src/types.rs @@ -6,77 +6,71 @@ use heck::ToSnakeCase; use weaveffi_core::abi::{self, CType}; use weaveffi_core::lang; use weaveffi_core::model::ParamBinding; +use weaveffi_core::model::Ty; use weaveffi_core::plan::{self, ArgPass, RetPass}; use weaveffi_core::utils::{local_type_name, wrapper_name}; -use weaveffi_ir::ir::TypeRef; /// The `ctypes` spelling of one *direct* (non-buffered) type's C slot. /// /// Buffered types (records, rich enums, lists, maps, and non-interface /// optionals) never occupy a scalar slot: they cross the ABI as one value /// buffer and are handled by the buffer codec paths instead. -pub(crate) fn py_ctypes_scalar(ty: &TypeRef) -> &'static str { +pub(crate) fn py_ctypes_scalar(ty: &Ty) -> &'static str { match ty { - TypeRef::I8 => "ctypes.c_int8", - TypeRef::I16 => "ctypes.c_int16", - TypeRef::I32 => "ctypes.c_int32", - TypeRef::U8 => "ctypes.c_uint8", - TypeRef::U16 => "ctypes.c_uint16", - TypeRef::U32 => "ctypes.c_uint32", - TypeRef::I64 => "ctypes.c_int64", - TypeRef::U64 => "ctypes.c_uint64", - TypeRef::F32 => "ctypes.c_float", - TypeRef::F64 => "ctypes.c_double", - TypeRef::Bool => "ctypes.c_int32", - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "ctypes.c_char_p", - TypeRef::Handle => "ctypes.c_uint64", + Ty::I8 => "ctypes.c_int8", + Ty::I16 => "ctypes.c_int16", + Ty::I32 => "ctypes.c_int32", + Ty::U8 => "ctypes.c_uint8", + Ty::U16 => "ctypes.c_uint16", + Ty::U32 => "ctypes.c_uint32", + Ty::I64 => "ctypes.c_int64", + Ty::U64 => "ctypes.c_uint64", + Ty::F32 => "ctypes.c_float", + Ty::F64 => "ctypes.c_double", + Ty::Bool => "ctypes.c_int32", + Ty::StringUtf8 | Ty::BorrowedStr => "ctypes.c_char_p", + Ty::Handle => "ctypes.c_uint64", // Typed handles, interfaces, and iterators cross as opaque pointers. - TypeRef::TypedHandle(_) | TypeRef::Interface(_) | TypeRef::Iterator(_) => "ctypes.c_void_p", - TypeRef::Bytes | TypeRef::BorrowedBytes => "ctypes.c_uint8", - TypeRef::Enum(_) => "ctypes.c_int32", - TypeRef::Record(_) - | TypeRef::RichEnum(_) - | TypeRef::Optional(_) - | TypeRef::List(_) - | TypeRef::Map(_, _) => { + Ty::TypedHandle(_) | Ty::Interface(_) | Ty::Iterator(_) => "ctypes.c_void_p", + Ty::Bytes | Ty::BorrowedBytes => "ctypes.c_uint8", + Ty::Enum(_) => "ctypes.c_int32", + Ty::Record(_) | Ty::RichEnum(_) | Ty::Optional(_) | Ty::List(_) | Ty::Map(_, _) => { unreachable!("buffered types have no scalar ctypes slot") } - TypeRef::Named(_) => unreachable!("unresolved type reference"), } } /// The Python typing hint for `ty` as it appears in signatures and stubs. -pub(crate) fn py_type_hint(ty: &TypeRef) -> String { +pub(crate) fn py_type_hint(ty: &Ty) -> String { match ty { - TypeRef::I8 - | TypeRef::I16 - | TypeRef::I32 - | TypeRef::U8 - | TypeRef::U16 - | TypeRef::U32 - | TypeRef::I64 - | TypeRef::U64 - | TypeRef::Handle => "int".into(), + Ty::I8 + | Ty::I16 + | Ty::I32 + | Ty::U8 + | Ty::U16 + | Ty::U32 + | Ty::I64 + | Ty::U64 + | Ty::Handle => "int".into(), // A typed handle is an opaque pointer-sized token; Python surfaces it // as a plain integer address, exactly like an untyped handle. - TypeRef::TypedHandle(_) => "int".into(), - TypeRef::F32 | TypeRef::F64 => "float".into(), - TypeRef::Bool => "bool".into(), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "str".into(), - TypeRef::Bytes | TypeRef::BorrowedBytes => "bytes".into(), + Ty::TypedHandle(_) => "int".into(), + Ty::F32 | Ty::F64 => "float".into(), + Ty::Bool => "bool".into(), + Ty::StringUtf8 | Ty::BorrowedStr => "str".into(), + Ty::Bytes | Ty::BorrowedBytes => "bytes".into(), // Records, rich enums, plain enums, and interfaces surface as bare // local class names in the generated module. A cross-module reference // (e.g. `types.Contact`) must still annotate the *local* `Contact`, // not the qualified IR name, which is not a symbol in this module. - TypeRef::Enum(name) => format!("\"{}\"", local_type_name(name)), - TypeRef::Record(name) | TypeRef::RichEnum(name) | TypeRef::Interface(name) => { + Ty::Enum(name) => format!("\"{}\"", local_type_name(name)), + Ty::Record(name) | Ty::RichEnum(name) | Ty::Interface(name) => { format!("\"{}\"", local_type_name(name)) } - TypeRef::Named(_) => unreachable!("unresolved type reference"), - TypeRef::Optional(inner) => format!("Optional[{}]", py_type_hint(inner)), - TypeRef::List(inner) => format!("List[{}]", py_type_hint(inner)), - TypeRef::Map(k, v) => format!("Dict[{}, {}]", py_type_hint(k), py_type_hint(v)), - TypeRef::Iterator(inner) => format!("Iterator[{}]", py_type_hint(inner)), + 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)), } } @@ -132,11 +126,11 @@ pub(crate) fn py_param_argtypes(p: &ParamBinding) -> Vec { } /// Returns `(restype, out_param_argtypes)` for a return type. -pub(crate) fn py_return_info(ty: &TypeRef) -> (String, Vec) { +pub(crate) fn py_return_info(ty: &Ty) -> (String, Vec) { // Iterator constructors return the opaque iterator handle; the `_next` // signature is emitted separately by the iterator code path (and // `ret_pass` deliberately rejects iterators). - if matches!(ty, TypeRef::Iterator(_)) { + if matches!(ty, Ty::Iterator(_)) { return ("ctypes.c_void_p".into(), vec![]); } // Module and prefix only shape an object return's destroy symbol, which @@ -175,6 +169,14 @@ pub(crate) fn py_field(name: &str) -> String { lang::escape_ident(name, lang::PYTHON_KEYWORDS) } +/// The Python spelling of an enum variant, used both as the `IntEnum` member +/// and as the suffix of a rich enum's per-variant dataclass. Variants are +/// PascalCase, so only the capitalized keywords (`None`, `True`, `False`) +/// can collide; `None = 0` inside a class body is a `SyntaxError`. +pub(crate) fn py_variant(name: &str) -> String { + lang::escape_ident(name, lang::PYTHON_KEYWORDS) +} + /// The Python spelling of an interface member name (method, static, or /// factory constructor): snake_case, keyword-escaped. pub(crate) fn py_member_name(name: &str) -> String { diff --git a/crates/weaveffi-gen-ruby/Cargo.toml b/crates/weaveffi-gen-ruby/Cargo.toml index 6d6579de..76e7d498 100644 --- a/crates/weaveffi-gen-ruby/Cargo.toml +++ b/crates/weaveffi-gen-ruby/Cargo.toml @@ -11,13 +11,9 @@ categories = ["development-tools::ffi"] [dependencies] weaveffi-core = { path = "../weaveffi-core", version = "0.21.0" } -weaveffi-ir = { path = "../weaveffi-ir", version = "0.21.0" } camino = { workspace = true } heck = { workspace = true } serde = { workspace = true } -[dev-dependencies] -tempfile = { workspace = true } - [lints] workspace = true diff --git a/crates/weaveffi-gen-ruby/src/calls.rs b/crates/weaveffi-gen-ruby/src/calls.rs index 4576d7e8..5e497867 100644 --- a/crates/weaveffi-gen-ruby/src/calls.rs +++ b/crates/weaveffi-gen-ruby/src/calls.rs @@ -4,19 +4,19 @@ //! //! Marshalling dispatch goes through the shared plan layer //! ([`ArgPass`], [`RetPass`], [`ElemFree`]) rather than crate-local -//! `TypeRef` folds, so this backend cannot drift from the others on +//! `Ty` folds, so this backend cannot drift from the others on //! call-boundary semantics. use heck::ToSnakeCase; use weaveffi_core::abi::CType; use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ AsyncBinding, CallShape, CallbackBinding, FnBinding, IteratorBinding, ListenerBinding, ModuleBinding, ParamBinding, }; use weaveffi_core::plan::{self, ArgPass, ElemFree, ErrorStrategy, RetPass}; use weaveffi_core::utils::{local_type_name, wrapper_name}; -use weaveffi_ir::ir::TypeRef; use crate::docs::{emit_doc, emit_param_docs}; use crate::entities::{rb_checker_name, rb_error_factory_name}; @@ -384,7 +384,7 @@ fn rb_cb_arg_expr(p: &ParamBinding) -> String { } => n, ArgPass::Object { nullable: true, .. } => format!("({n}.null? ? nil : {n})"), ArgPass::Direct { .. } => match &p.ty { - TypeRef::Bool => format!("({n} != 0)"), + Ty::Bool => format!("({n} != 0)"), // Scalars, enums (their integer constants), and handles pass raw. _ => n, }, @@ -623,7 +623,7 @@ fn render_async_function_wrapper( /// finalizer-bearing wrapper instead. fn render_async_result_push( out: &mut String, - ret: &Option, + ret: &Option, ind: &str, qualifier: Option<&str>, ) { @@ -678,9 +678,9 @@ fn render_async_result_push( // return surfaces nil for null instead of trapping. RetPass::Object { nullable, .. } => { let name = match ret.as_ref() { - Some(TypeRef::Interface(name)) => name, - Some(TypeRef::Optional(inner)) => match inner.as_ref() { - TypeRef::Interface(name) => name, + Some(Ty::Interface(name)) => name, + Some(Ty::Optional(inner)) => match inner.as_ref() { + Ty::Interface(name) => name, _ => unreachable!("only optional interfaces adopt object returns"), }, _ => unreachable!("object return must be an interface type"), @@ -703,10 +703,10 @@ fn render_async_result_push( } } RetPass::Direct => match ret.as_ref() { - Some(TypeRef::Bool) => { + Some(Ty::Bool) => { w.line("queue << (result != 0)"); } - Some(TypeRef::TypedHandle(name)) => { + Some(Ty::TypedHandle(name)) => { let local = local_type_name(name); w.line("if result.null?"); w.scope(|w| { @@ -848,15 +848,10 @@ fn render_iterator_function_wrapper( /// cannot leak the element). `qualifier` is the top-level Ruby module name /// when rendering inside a class body, where `weaveffi_free_*` calls need an /// explicit receiver. -fn render_iterator_item_yield( - out: &mut String, - elem: &TypeRef, - ind: &str, - qualifier: Option<&str>, -) { +fn render_iterator_item_yield(out: &mut String, elem: &Ty, ind: &str, qualifier: Option<&str>) { let m = qualifier.map(|q| format!("{q}.")).unwrap_or_default(); let mut w = CodeWriter::two_space().with_depth(ind.len() / 2); - if weaveffi_core::abi::is_buffered(elem) { + if elem.is_buffered() { // A buffered element is a producer-allocated value buffer: copy the // bytes, release them, then decode and yield the value. w.line("item_ptr = out_item.read_pointer"); @@ -873,7 +868,7 @@ fn render_iterator_item_yield( return; } match elem { - TypeRef::StringUtf8 | TypeRef::BorrowedStr => { + Ty::StringUtf8 | Ty::BorrowedStr => { w.line("item_ptr = out_item.read_pointer"); w.line("if item_ptr.null?"); w.scope(|w| { @@ -887,7 +882,7 @@ fn render_iterator_item_yield( }); w.line("end"); } - TypeRef::Bytes | TypeRef::BorrowedBytes => { + Ty::Bytes | Ty::BorrowedBytes => { w.line("item_ptr = out_item.read_pointer"); w.line("item_len = out_item_len.read(:size_t)"); w.line("if item_ptr.null?"); @@ -904,21 +899,21 @@ fn render_iterator_item_yield( } // A yielded typed handle is a new owned reference; the wrapper adopts // the pointer. - TypeRef::TypedHandle(name) => { + Ty::TypedHandle(name) => { let local = local_type_name(name); w.line("item_ptr = out_item.read_pointer"); w.line(format!("y << {local}.new(item_ptr) unless item_ptr.null?")); } // A yielded interface is a new owned reference; wrap it without // re-running initialize. - TypeRef::Interface(name) => { + Ty::Interface(name) => { let local = local_type_name(name); w.line("item_ptr = out_item.read_pointer"); w.line(format!( "y << {local}._from_ptr(item_ptr) unless item_ptr.null?" )); } - TypeRef::Bool => { + Ty::Bool => { w.line("y << (out_item.read_int32 != 0)"); } _ => { @@ -945,10 +940,10 @@ fn rb_call_args(p: &ParamBinding) -> Vec { // A nullable borrowed object pointer: nil passes as NULL. ArgPass::Object { nullable: true, .. } => vec![format!("{name}&.handle")], ArgPass::Direct { .. } => match &p.ty { - TypeRef::Bool => vec![format!("{name}_c")], + Ty::Bool => vec![format!("{name}_c")], // Typed handles occupy an opaque pointer slot borrowed from the // wrapper object. - TypeRef::TypedHandle(_) => vec![format!("{name}.handle")], + Ty::TypedHandle(_) => vec![format!("{name}.handle")], _ => vec![name], }, } @@ -980,7 +975,7 @@ fn render_param_conversion(out: &mut String, p: &ParamBinding, ind: &str, qualif )); w.line(format!("{name}_buf.put_bytes(0, {name})")); } - ArgPass::Direct { .. } if matches!(p.ty, TypeRef::Bool) => { + ArgPass::Direct { .. } if matches!(p.ty, Ty::Bool) => { w.line(format!("{name}_c = {name} ? 1 : 0")); } _ => {} @@ -993,7 +988,7 @@ fn render_param_conversion(out: &mut String, p: &ParamBinding, ind: &str, qualif /// [`RetPass`] contract. A buffered return is a producer-allocated value /// buffer paired with `out_len`: the bytes are copied, released with /// `weaveffi_free_bytes`, then decoded. -fn render_return_code(out: &mut String, ty: &TypeRef, ind: &str, qualifier: Option<&str>) { +fn render_return_code(out: &mut String, ty: &Ty, ind: &str, qualifier: Option<&str>) { let m = qualifier.map(|q| format!("{q}.")).unwrap_or_default(); let mut w = CodeWriter::two_space().with_depth(ind.len() / 2); match plan::ret_pass(Some(ty), "", "") { @@ -1027,9 +1022,9 @@ fn render_return_code(out: &mut String, ty: &TypeRef, ind: &str, qualifier: Opti // return surfaces nil for null. RetPass::Object { nullable, .. } => { let name = match ty { - TypeRef::Interface(name) => name, - TypeRef::Optional(inner) => match inner.as_ref() { - TypeRef::Interface(name) => name, + Ty::Interface(name) => name, + Ty::Optional(inner) => match inner.as_ref() { + Ty::Interface(name) => name, _ => unreachable!("only optional interfaces adopt object returns"), }, _ => unreachable!("object return must be an interface type"), @@ -1044,10 +1039,10 @@ fn render_return_code(out: &mut String, ty: &TypeRef, ind: &str, qualifier: Opti } } RetPass::Direct => match ty { - TypeRef::Bool => { + Ty::Bool => { w.line("result != 0"); } - TypeRef::TypedHandle(name) => { + Ty::TypedHandle(name) => { w.line("raise Error.new(-1, 'null pointer') if result.null?"); w.line(format!("{name}.new(result)")); } diff --git a/crates/weaveffi-gen-ruby/src/codec.rs b/crates/weaveffi-gen-ruby/src/codec.rs index b0ddd698..34fcbae3 100644 --- a/crates/weaveffi-gen-ruby/src/codec.rs +++ b/crates/weaveffi-gen-ruby/src/codec.rs @@ -8,10 +8,10 @@ use heck::ToSnakeCase; use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{EnumBinding, StructBinding}; +use weaveffi_core::model::{Prim, WireType}; use weaveffi_core::utils::local_type_name; -use weaveffi_core::wire::{self, WireType}; -use weaveffi_ir::ir::TypeRef; use crate::types::rb_field_name; @@ -27,19 +27,19 @@ pub(crate) fn wv_stem(name: &str) -> String { /// encode as their `i32` discriminant; handles as raw `u64` tokens. fn wv_scalar_writer(shape: &WireType) -> Option<&'static str> { Some(match shape { - WireType::Bool => "write_bool", - WireType::I8 => "write_i8", - WireType::U8 => "write_u8", - WireType::I16 => "write_i16", - WireType::U16 => "write_u16", - WireType::I32 | WireType::Enum(_) => "write_i32", - WireType::U32 => "write_u32", - WireType::I64 => "write_i64", - WireType::U64 | WireType::Handle => "write_u64", - WireType::F32 => "write_f32", - WireType::F64 => "write_f64", - WireType::String => "write_string", - WireType::Bytes => "write_bytes", + WireType::Prim(Prim::Bool) => "write_bool", + WireType::Prim(Prim::I8) => "write_i8", + WireType::Prim(Prim::U8) => "write_u8", + WireType::Prim(Prim::I16) => "write_i16", + WireType::Prim(Prim::U16) => "write_u16", + WireType::Prim(Prim::I32) | WireType::Enum(_) => "write_i32", + WireType::Prim(Prim::U32) => "write_u32", + WireType::Prim(Prim::I64) => "write_i64", + WireType::Prim(Prim::U64) | WireType::Handle(_) => "write_u64", + WireType::Prim(Prim::F32) => "write_f32", + WireType::Prim(Prim::F64) => "write_f64", + WireType::Prim(Prim::String) => "write_string", + WireType::Prim(Prim::Bytes) => "write_bytes", _ => return None, }) } @@ -48,19 +48,19 @@ fn wv_scalar_writer(shape: &WireType) -> Option<&'static str> { /// [`wv_scalar_writer`]. fn wv_scalar_reader(shape: &WireType) -> Option<&'static str> { Some(match shape { - WireType::Bool => "read_bool", - WireType::I8 => "read_i8", - WireType::U8 => "read_u8", - WireType::I16 => "read_i16", - WireType::U16 => "read_u16", - WireType::I32 | WireType::Enum(_) => "read_i32", - WireType::U32 => "read_u32", - WireType::I64 => "read_i64", - WireType::U64 | WireType::Handle => "read_u64", - WireType::F32 => "read_f32", - WireType::F64 => "read_f64", - WireType::String => "read_string", - WireType::Bytes => "read_bytes", + WireType::Prim(Prim::Bool) => "read_bool", + WireType::Prim(Prim::I8) => "read_i8", + WireType::Prim(Prim::U8) => "read_u8", + WireType::Prim(Prim::I16) => "read_i16", + WireType::Prim(Prim::U16) => "read_u16", + WireType::Prim(Prim::I32) | WireType::Enum(_) => "read_i32", + WireType::Prim(Prim::U32) => "read_u32", + WireType::Prim(Prim::I64) => "read_i64", + WireType::Prim(Prim::U64) | WireType::Handle(_) => "read_u64", + WireType::Prim(Prim::F32) => "read_f32", + WireType::Prim(Prim::F64) => "read_f64", + WireType::Prim(Prim::String) => "read_string", + WireType::Prim(Prim::Bytes) => "read_bytes", _ => return None, }) } @@ -73,11 +73,11 @@ pub(crate) fn render_wv_write( w: &mut CodeWriter, wvar: &str, expr: &str, - ty: &TypeRef, + ty: &Ty, depth: usize, q: &str, ) { - let shape = wire::classify(ty); + let shape = ty.wire(); if let Some(m) = wv_scalar_writer(&shape) { w.line(format!("{wvar}.{m}({expr})")); return; @@ -125,11 +125,11 @@ pub(crate) fn render_wv_read( w: &mut CodeWriter, rvar: &str, var: &str, - ty: &TypeRef, + ty: &Ty, depth: usize, q: &str, ) { - let shape = wire::classify(ty); + let shape = ty.wire(); if let Some(m) = wv_scalar_reader(&shape) { w.line(format!("{var} = {rvar}.{m}")); return; diff --git a/crates/weaveffi-gen-ruby/src/lib.rs b/crates/weaveffi-gen-ruby/src/lib.rs index a776bcd4..61646625 100644 --- a/crates/weaveffi-gen-ruby/src/lib.rs +++ b/crates/weaveffi-gen-ruby/src/lib.rs @@ -17,9 +17,6 @@ mod package; mod runtime; mod types; -#[cfg(test)] -mod tests; - use camino::Utf8Path; use serde::{Deserialize, Serialize}; use weaveffi_core::backend::{LanguageBackend, OutputFile}; @@ -219,8 +216,6 @@ impl LanguageBackend for RubyGenerator { } } -weaveffi_core::impl_generator_via_backend!(RubyGenerator); - /// Render the primary Ruby library source: the fixed preamble, then each /// module's typed error surface, entities, codecs, FFI attachments, and /// wrappers, in dependency order. diff --git a/crates/weaveffi-gen-ruby/src/runtime.rs b/crates/weaveffi-gen-ruby/src/runtime.rs index 937eddfc..859314f8 100644 --- a/crates/weaveffi-gen-ruby/src/runtime.rs +++ b/crates/weaveffi-gen-ruby/src/runtime.rs @@ -1,7 +1,9 @@ //! The fixed Ruby runtime the generated module carries: the library loader, //! the error surface, the value-buffer reader/writer pair, and the runtime //! ABI attachments (`error_clear`, `error_free`, `free_string`, -//! `free_bytes`). +//! `free_bytes`), guarded by the load-time ABI-revision check. + +use weaveffi_core::cabi::ABI_VERSION; /// The exact `ffi_lib` loader block [`render_preamble`] emits in `generate` /// mode, so the packager can swap it for a bundled-first variant. @@ -61,6 +63,22 @@ module {module_name} {RUBY_LOADER_ORIGINAL} + # The ABI revision these bindings were generated against. Checked before any + # other symbol is attached so a mismatched producer fails at require time + # instead of misreading the error struct or a value buffer later. + ABI_VERSION = {ABI_VERSION} + begin + attach_function :weaveffi_abi_version, [], :uint32 + rescue FFI::NotFoundError + raise LoadError, 'the loaded WeaveFFI library predates ABI versioning ' \\ + \"(these bindings expect ABI revision #{{ABI_VERSION}})\" + end + _wv_abi = weaveffi_abi_version + unless _wv_abi == ABI_VERSION + raise LoadError, \"WeaveFFI ABI mismatch: these bindings expect revision #{{ABI_VERSION}} \" \\ + \"but the loaded library reports revision #{{_wv_abi}}\" + end + class ErrorStruct < FFI::Struct layout :code, :int32, :message, :pointer, diff --git a/crates/weaveffi-gen-ruby/src/tests.rs b/crates/weaveffi-gen-ruby/src/tests.rs deleted file mode 100644 index b885282a..00000000 --- a/crates/weaveffi-gen-ruby/src/tests.rs +++ /dev/null @@ -1,3301 +0,0 @@ -//! Tests for the Ruby generator: rendering coverage across every entity -//! and call shape, error-domain semantics, packaging, and identifier policy. - -use super::*; -use crate::types::{rb_abi_types, rb_ffi_type}; -use camino::Utf8Path; -use weaveffi_core::abi; -use weaveffi_core::codegen::Generator; -use weaveffi_core::resolved::ResolvedApi; - -#[test] -fn package_emits_platform_gems_and_swaps_loader() { - use weaveffi_core::package::{FileContent, PackageContext}; - use weaveffi_core::platform::{BinarySet, Platform}; - - let api = make_api(vec![simple_module( - "calc", - vec![Function { - name: "ping".into(), - params: vec![], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - let model = BindingModel::build(&api, "weaveffi"); - let mut bins = BinarySet::new("calculator"); - bins.insert(Platform::MacosArm64, "/s/darwin-arm64/libcalculator.dylib"); - bins.insert(Platform::LinuxX64, "/s/linux-x64/libcalculator.so"); - let ctx = PackageContext { - binaries: &bins, - input_basename: Some("calculator.yml"), - }; - let files = LanguageBackend::package( - &RubyGenerator, - &api, - &model, - &ctx, - Utf8Path::new("/out"), - &RubyConfig::default(), - ) - .expect("ruby supports packaging"); - - assert_eq!(files.iter().filter(|f| f.is_binary()).count(), 2); - // Bundled under lib/native/ inside each per-platform gem dir. - assert!(files.iter().any(|f| f - .path - .as_str() - .ends_with("ruby/darwin-arm64/lib/native/libcalculator.dylib"))); - // The gemspec stamps the RubyGems platform string. - let gemspec = files - .iter() - .find(|f| f.path.as_str().ends_with("darwin-arm64/weaveffi.gemspec")) - .expect("gemspec present"); - let FileContent::Text(spec) = &gemspec.content else { - panic!("gemspec is text"); - }; - assert!( - spec.contains("s.platform = 'arm64-darwin'"), - "platform: {spec}" - ); - // The loader was rewritten to prefer the bundled library. - let rb = files - .iter() - .find(|f| f.path.as_str().ends_with("darwin-arm64/lib/weaveffi.rb")) - .expect("library module present"); - let FileContent::Text(src) = &rb.content else { - panic!("module is text"); - }; - assert!( - src.contains("File.exist?") && src.contains("libcalculator.dylib"), - "packaged loader not applied: {src}" - ); -} -use weaveffi_ir::ir::{ - Api, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, InterfaceDef, Module, Param, - StructDef, StructField, TypeRef, -}; - -fn make_api(modules: Vec) -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: "0.7.0".to_string(), - modules, - generators: None, - package: None, - }) -} - -fn simple_module(name: &str, functions: Vec) -> Module { - Module { - name: name.into(), - functions, - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - } -} - -/// Build the model (test-only; the driver builds it in production) and -/// render with the default naming (module-prefix stripping on). -fn render(api: &ResolvedApi, module_name: &str, prefix: &str) -> String { - let model = BindingModel::build(api, prefix); - render_ruby_module(&model, module_name, true, "weaveffi.rb", "weaveffi.yml") -} - -/// A function literal with the boilerplate zeroed; tests override the -/// fields they exercise. -fn plain_fn(name: &str, params: Vec, returns: Option) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - } -} - -fn str_param(name: &str) -> Param { - Param { - name: name.into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - } -} - -/// A `kv` module with a declared error domain, an interface with a `new` -/// constructor, a factory constructor, methods (sync throwing, sync -/// non-throwing, async), a static, plus throwing and non-throwing free -/// functions. -fn kv_api() -> ResolvedApi { - let mut m = simple_module( - "kv", - vec![ - { - let mut f = plain_fn( - "kv_lookup", - vec![str_param("key")], - Some(TypeRef::StringUtf8), - ); - f.throws = true; - f - }, - plain_fn("kv_ping", vec![], Some(TypeRef::Bool)), - ], - ); - m.errors = Some(ErrorDomain { - name: "KvError".into(), - codes: vec![ - ErrorCode { - name: "KeyNotFound".into(), - code: 1001, - message: "key not found".into(), - doc: Some("Raised when the key is absent.".into()), - fields: vec![], - }, - ErrorCode { - name: "IoError".into(), - code: 1004, - message: "I/O failure".into(), - doc: None, - fields: vec![], - }, - ], - }); - m.interfaces = vec![InterfaceDef { - name: "Store".into(), - doc: Some("A key-value store.".into()), - constructors: vec![ - { - let mut f = plain_fn("new", vec![str_param("path")], None); - f.throws = true; - f - }, - { - let mut f = plain_fn("open", vec![str_param("path")], None); - f.throws = true; - f - }, - ], - methods: vec![ - { - let mut f = plain_fn("put", vec![str_param("key"), str_param("value")], None); - f.throws = true; - f - }, - plain_fn("count", vec![], Some(TypeRef::U32)), - { - let mut f = plain_fn("compact", vec![], Some(TypeRef::Bool)); - f.r#async = true; - f.cancellable = true; - f.throws = true; - f - }, - ], - statics: vec![plain_fn("default_capacity", vec![], Some(TypeRef::U32))], - }]; - make_api(vec![m]) -} - -#[test] -fn name_returns_ruby() { - assert_eq!(Generator::name(&RubyGenerator), "ruby"); -} - -#[test] -fn interface_ffi_attaches_destroy_and_members() { - let code = render(&kv_api(), "WeaveFFI", "weaveffi"); - assert!( - code.contains("attach_function :weaveffi_kv_Store_destroy, [:pointer], :void"), - "destroy attach: {code}" - ); - assert!( - code.contains("attach_function :weaveffi_kv_Store_new, [:string, :pointer], :pointer"), - "ctor attach: {code}" - ); - assert!( - code.contains( - "attach_function :weaveffi_kv_Store_put, [:pointer, :string, :string, :pointer], :void" - ), - "method attach includes self slot: {code}" - ); - assert!( - code.contains("attach_function :weaveffi_kv_Store_default_capacity, [:pointer], :uint32"), - "static attach has no self slot: {code}" - ); -} - -#[test] -fn interface_class_wraps_pointer_with_auto_pointer() { - let code = render(&kv_api(), "WeaveFFI", "weaveffi"); - assert!( - code.contains("class StorePtr < FFI::AutoPointer"), - "AutoPointer subclass: {code}" - ); - assert!( - code.contains("WeaveFFI.weaveffi_kv_Store_destroy(ptr)"), - "release calls destroy symbol: {code}" - ); - assert!(code.contains("def destroy"), "explicit destroy: {code}"); - assert!(code.contains("@handle.free"), "destroy frees: {code}"); - assert!( - code.contains("def self._from_ptr(ptr)") && code.contains("obj = allocate"), - "_from_ptr avoids initialize: {code}" - ); -} - -#[test] -fn interface_new_ctor_maps_to_initialize() { - let code = render(&kv_api(), "WeaveFFI", "weaveffi"); - assert!(code.contains("def initialize(path)"), "initialize: {code}"); - assert!( - code.contains("result = WeaveFFI.weaveffi_kv_Store_new(path, err)"), - "ctor call: {code}" - ); - assert!( - code.contains("@handle = StorePtr.new(result)"), - "handle assignment: {code}" - ); -} - -#[test] -fn interface_named_ctor_is_class_method_factory() { - let code = render(&kv_api(), "WeaveFFI", "weaveffi"); - assert!(code.contains("def self.open(path)"), "factory def: {code}"); - assert!( - code.contains("result = WeaveFFI.weaveffi_kv_Store_open(path, err)"), - "factory call: {code}" - ); - assert!( - code.contains("_from_ptr(result)"), - "factory wraps without initialize: {code}" - ); -} - -#[test] -fn interface_method_passes_handle_first() { - let code = render(&kv_api(), "WeaveFFI", "weaveffi"); - assert!(code.contains("def put(key, value)"), "method def: {code}"); - assert!( - code.contains("WeaveFFI.weaveffi_kv_Store_put(@handle, key, value, err)"), - "self slot leads: {code}" - ); -} - -#[test] -fn interface_static_is_class_method() { - let code = render(&kv_api(), "WeaveFFI", "weaveffi"); - assert!( - code.contains("def self.default_capacity()"), - "static def: {code}" - ); - assert!( - code.contains("result = WeaveFFI.weaveffi_kv_Store_default_capacity(err)"), - "static call has no self slot: {code}" - ); -} - -#[test] -fn typed_error_classes_and_helpers() { - let code = render(&kv_api(), "WeaveFFI", "weaveffi"); - assert!(code.contains("class KvError < Error"), "domain: {code}"); - assert!( - code.contains("class KeyNotFound < KvError"), - "code subclass: {code}" - ); - assert!(code.contains("CODE = 1001"), "code constant: {code}"); - assert!( - code.contains("def initialize(message = 'key not found')"), - "default message: {code}" - ); - assert!( - code.contains("1004 => KvError::IoError,"), - "code table: {code}" - ); - assert!( - code.contains("def self.kv_error_from(code, message, payload = nil)"), - "factory helper: {code}" - ); - assert!( - code.contains("def self.check_kv_error!(err)"), - "checker helper: {code}" - ); - assert!( - code.contains("raise kv_error_from(code, msg, payload)"), - "checker raises typed: {code}" - ); - assert!( - code.contains( - "payload = payload_ptr.null? ? nil : payload_ptr.read_string(err[:payload_len])" - ), - "checker copies payload before clearing: {code}" - ); -} - -#[test] -fn error_payload_fields_decode_into_attributes() { - let mut m = simple_module("kv", { - let mut f = plain_fn("kv_load", vec![str_param("key")], None); - f.throws = true; - vec![f] - }); - m.errors = Some(ErrorDomain { - name: "KvError".into(), - codes: vec![ErrorCode { - name: "KeyNotFound".into(), - code: 1001, - message: "key not found".into(), - doc: None, - fields: vec![ - StructField { - name: "key".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "attempts".into(), - ty: TypeRef::U32, - doc: None, - }, - ], - }], - }); - let code = render(&make_api(vec![m]), "WeaveFFI", "weaveffi"); - // The exception class exposes the payload fields as attributes. - assert!( - code.contains("class KeyNotFound < KvError"), - "code subclass: {code}" - ); - assert!( - code.contains("attr_reader :key") && code.contains("attr_reader :attempts"), - "payload attrs: {code}" - ); - assert!( - code.contains("def initialize(message = 'key not found', key: nil, attempts: nil)"), - "kwargs initialize: {code}" - ); - // The factory decodes the value-buffer payload in declaration order. - assert!(code.contains("when 1001"), "payload dispatch: {code}"); - assert!( - code.contains("r = WvBufferReader.new(payload || ''.b)"), - "payload reader: {code}" - ); - assert!( - code.contains("_wv_key = r.read_string") && code.contains("_wv_attempts = r.read_u32"), - "field decode: {code}" - ); - assert!( - code.contains("KvError::KeyNotFound.new(message, key: _wv_key, attempts: _wv_attempts)"), - "typed construction: {code}" - ); -} - -#[test] -fn throwing_function_uses_typed_checker() { - let code = render(&kv_api(), "WeaveFFI", "weaveffi"); - let lookup = code - .split("def self.kv_lookup(key)") - .nth(1) - .expect("kv_lookup wrapper"); - assert!( - lookup.contains("check_kv_error!(err)"), - "typed checker: {code}" - ); -} - -#[test] -fn non_throwing_function_uses_generic_checker() { - let code = render(&kv_api(), "WeaveFFI", "weaveffi"); - let ping = code - .split("def self.kv_ping()") - .nth(1) - .expect("kv_ping wrapper"); - let body = ping.split("\n end").next().expect("wrapper body"); - assert!(body.contains("check_error!(err)"), "generic: {code}"); - assert!( - !body.contains("check_kv_error!"), - "no typed checker: {code}" - ); -} - -#[test] -fn non_throwing_method_uses_generic_checker() { - let code = render(&kv_api(), "WeaveFFI", "weaveffi"); - let count = code.split("def count()").nth(1).expect("count wrapper"); - let body = count.split("\n end").next().expect("method body"); - assert!( - body.contains("WeaveFFI.check_error!(err)"), - "generic qualified: {code}" - ); - assert!( - !body.contains("check_kv_error!"), - "no typed checker: {code}" - ); -} - -#[test] -fn async_member_routes_typed_error_and_self_slot() { - let code = render(&kv_api(), "WeaveFFI", "weaveffi"); - let compact = code.split("def compact()").nth(1).expect("compact wrapper"); - assert!( - compact.contains("queue << WeaveFFI.kv_error_from(code, msg, payload)"), - "typed async error: {code}" - ); - assert!( - compact.contains( - "WeaveFFI.weaveffi_kv_Store_compact_async(@handle, FFI::Pointer::NULL, callback, FFI::Pointer::NULL)" - ), - "self slot then cancel token: {code}" - ); -} - -#[test] -fn interface_params_borrow_and_returns_wrap() { - let mut m = simple_module( - "kv", - vec![ - plain_fn( - "clone_store", - vec![Param { - name: "store".into(), - ty: TypeRef::Interface("Store".into()), - mutable: false, - doc: None, - }], - Some(TypeRef::Interface("Store".into())), - ), - plain_fn( - "find_store", - vec![], - Some(TypeRef::Optional(Box::new(TypeRef::Interface( - "Store".into(), - )))), - ), - ], - ); - m.interfaces = vec![InterfaceDef { - name: "Store".into(), - doc: None, - constructors: vec![plain_fn("new", vec![], None)], - methods: vec![], - statics: vec![], - }]; - let code = render(&make_api(vec![m]), "WeaveFFI", "weaveffi"); - assert!( - code.contains("weaveffi_kv_clone_store(store.handle, err)"), - "param borrows handle: {code}" - ); - assert!( - code.contains("Store._from_ptr(result)"), - "return wraps owned pointer: {code}" - ); - let find = code - .split("def self.find_store()") - .nth(1) - .expect("find_store wrapper"); - assert!( - find.contains("return nil if result.null?"), - "optional interface nil: {code}" - ); -} - -#[test] -fn naming_strips_module_prefix_by_default() { - let api = make_api(vec![simple_module( - "kv", - vec![plain_fn("open_store", vec![], None)], - )]); - let code = render(&api, "WeaveFFI", "weaveffi"); - assert!( - code.contains("def self.open_store()"), - "stripped name: {code}" - ); - assert!( - !code.contains("def self.kv_open_store()"), - "no prefixed wrapper: {code}" - ); - // The C symbol stays fully qualified regardless of wrapper naming. - assert!( - code.contains("weaveffi_kv_open_store(err)"), - "C symbol: {code}" - ); -} - -#[test] -fn naming_knob_restores_prefixed_wrappers() { - let api = make_api(vec![simple_module( - "kv", - vec![plain_fn("open_store", vec![], None)], - )]); - let model = BindingModel::build(&api, "weaveffi"); - let code = render_ruby_module(&model, "WeaveFFI", false, "weaveffi.rb", "weaveffi.yml"); - assert!( - code.contains("def self.kv_open_store()"), - "prefixed name: {code}" - ); -} - -#[test] -fn throwing_iterator_uses_typed_checker() { - let mut m = simple_module("kv", { - let mut f = plain_fn( - "scan", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - ); - f.throws = true; - vec![f] - }); - m.errors = Some(ErrorDomain { - name: "KvError".into(), - codes: vec![ErrorCode { - name: "IoError".into(), - code: 1004, - message: "I/O failure".into(), - doc: None, - fields: vec![], - }], - }); - let code = render(&make_api(vec![m]), "WeaveFFI", "weaveffi"); - let scan = code.split("def self.scan()").nth(1).expect("scan wrapper"); - assert!( - scan.contains("check_kv_error!(err)"), - "launch checker: {code}" - ); - assert!( - scan.contains("check_kv_error!(item_err)"), - "next checker: {code}" - ); -} - -#[test] -fn generates_output_file() { - let api = make_api(vec![simple_module( - "math", - vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - RubyGenerator - .generate(&api, out_dir, &RubyConfig::default()) - .unwrap(); - - let file = out_dir.join("ruby/lib/weaveffi.rb"); - assert!(file.exists(), "weaveffi.rb should exist"); - let contents = std::fs::read_to_string(&file).unwrap(); - assert!(contents.contains("require 'ffi'")); - assert!(contents.contains("module WeaveFFI")); - assert!(contents.contains("attach_function :weaveffi_math_add")); - assert!(contents.contains("def self.add(a, b)")); -} - -#[test] -fn output_files_returns_correct_path() { - let api = make_api(vec![]); - let out_dir = Utf8Path::new("/tmp/out"); - let files = RubyGenerator.output_files(&api, out_dir, &RubyConfig::default()); - assert_eq!( - files, - vec![ - format!("{out_dir}/ruby/README.md"), - format!("{out_dir}/ruby/lib/weaveffi.rb"), - format!("{out_dir}/ruby/weaveffi.gemspec"), - ] - ); -} - -#[test] -fn ruby_generates_gemspec() { - let api = make_api(vec![simple_module("math", vec![])]); - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - RubyGenerator - .generate(&api, out_dir, &RubyConfig::default()) - .unwrap(); - - let gemspec = out_dir.join("ruby/weaveffi.gemspec"); - assert!(gemspec.exists(), "gemspec should exist"); - let contents = std::fs::read_to_string(&gemspec).unwrap(); - assert!( - contents.contains("Gem::Specification.new do |s|"), - "gemspec header: {contents}" - ); - assert!(contents.contains("s.name"), "name field: {contents}"); - assert!(contents.contains("s.version"), "version field: {contents}"); - assert!(contents.contains("s.summary"), "summary field: {contents}"); - assert!(contents.contains("s.files"), "files field: {contents}"); - assert!( - contents.contains("s.require_paths"), - "require_paths: {contents}" - ); - assert!( - contents.contains("s.add_dependency 'ffi', '~> 1.15'"), - "ffi dependency: {contents}" - ); - - let readme = out_dir.join("ruby/README.md"); - assert!(readme.exists(), "README should exist"); - let readme_contents = std::fs::read_to_string(&readme).unwrap(); - assert!( - readme_contents.contains("gem build"), - "usage instructions: {readme_contents}" - ); -} - -#[test] -fn renders_enum_with_shouty_snake_case() { - let api = make_api(vec![Module { - name: "gfx".into(), - functions: vec![], - interfaces: vec![], - structs: vec![], - enums: vec![EnumDef { - name: "Color".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "DarkBlue".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }]); - - let code = render(&api, "WeaveFFI", "weaveffi"); - assert!(code.contains("module Color"), "enum module: {code}"); - assert!(code.contains("RED = 0"), "RED: {code}"); - assert!(code.contains("DARK_BLUE = 1"), "DARK_BLUE: {code}"); -} - -#[test] -fn renders_struct_as_value_class() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![], - interfaces: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: None, - }, - StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }]); - - let code = render(&api, "WeaveFFI", "weaveffi"); - assert!(code.contains("class Contact"), "class: {code}"); - assert!(code.contains("attr_reader :id"), "id attr: {code}"); - assert!(code.contains("attr_reader :name"), "name attr: {code}"); - assert!( - code.contains("def initialize(id:, name:)"), - "kwargs initialize: {code}" - ); - assert!(code.contains("def ==(other)"), "structural eq: {code}"); - assert!( - code.contains("return false unless other.is_a?(Contact)"), - "eq type guard: {code}" - ); - // A record is a value type: no FFI pointer wrapping, no destroy, no - // create, and no C symbols at all. - assert!( - !code.contains("ContactPtr") && !code.contains("FFI::AutoPointer"), - "no pointer wrapper: {code}" - ); - assert!( - !code.contains("weaveffi_contacts_Contact_destroy"), - "no destroy symbol: {code}" - ); - assert!( - !code.contains("attach_function :weaveffi_contacts_Contact"), - "no record C symbols: {code}" - ); -} - -#[test] -fn struct_codec_packs_and_unpacks_fields_in_order() { - let api = make_api(vec![Module { - name: "geo".into(), - functions: vec![], - interfaces: vec![], - structs: vec![StructDef { - name: "Point".into(), - doc: None, - fields: vec![ - StructField { - name: "x".into(), - ty: TypeRef::F64, - doc: None, - }, - StructField { - name: "label".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }]); - - let code = render(&api, "WeaveFFI", "weaveffi"); - // One private pack/unpack pair per record, fields in wire order. - assert!( - code.contains("def self._wv_write_point(w, v)"), - "pack helper: {code}" - ); - let pack = code - .split("def self._wv_write_point(w, v)") - .nth(1) - .expect("pack body"); - let x_write = pack.find("w.write_f64(v.x)").expect("x write"); - let label_write = pack.find("w.write_string(v.label)").expect("label write"); - assert!(x_write < label_write, "declaration order: {code}"); - assert!( - code.contains("def self._wv_read_point(r)"), - "unpack helper: {code}" - ); - assert!( - code.contains("_wv_x = r.read_f64") && code.contains("_wv_label = r.read_string"), - "field reads: {code}" - ); - assert!( - code.contains("Point.new(x: _wv_x, label: _wv_label)"), - "unpack constructs value class: {code}" - ); - // Builders are gone entirely. - assert!(!code.contains("PointBuilder"), "no builder class: {code}"); - assert!( - !code.contains("weaveffi_geo_Point_create"), - "no create symbol: {code}" - ); -} - -#[test] -fn function_wrapper_checks_error() { - let api = make_api(vec![simple_module( - "math", - vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - - let code = render(&api, "WeaveFFI", "weaveffi"); - assert!(code.contains("err = ErrorStruct.new"), "err alloc: {code}"); - assert!(code.contains("check_error!(err)"), "check_error: {code}"); -} - -#[test] -fn string_return_reads_and_frees() { - let api = make_api(vec![simple_module( - "data", - vec![Function { - name: "get_name".into(), - params: vec![], - returns: Some(TypeRef::StringUtf8), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - - let code = render(&api, "WeaveFFI", "weaveffi"); - assert!(code.contains("result.read_string"), "read_string: {code}"); - assert!( - code.contains("weaveffi_free_string(result)"), - "free_string: {code}" - ); - assert!( - code.contains("return '' if result.null?"), - "null check: {code}" - ); -} - -#[test] -fn bool_param_and_return_conversion() { - let api = make_api(vec![simple_module( - "check", - vec![Function { - name: "is_valid".into(), - params: vec![Param { - name: "value".into(), - ty: TypeRef::Bool, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Bool), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - - let code = render(&api, "WeaveFFI", "weaveffi"); - assert!( - code.contains("value_c = value ? 1 : 0"), - "bool param: {code}" - ); - assert!(code.contains("result != 0"), "bool return: {code}"); -} - -#[test] -fn optional_string_returns_nil() { - let api = make_api(vec![simple_module( - "data", - vec![Function { - name: "find".into(), - params: vec![], - returns: Some(TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - - let code = render(&api, "WeaveFFI", "weaveffi"); - // An optional string is buffered: a flag byte selects nil or the value. - assert!(code.contains("if _wv_r.read_flag"), "flag byte: {code}"); - assert!( - code.contains("_wv_value = _wv_r.read_string"), - "present decode: {code}" - ); - assert!(code.contains("_wv_value = nil"), "absent is nil: {code}"); - assert!( - code.contains("weaveffi_free_bytes(result, len) unless result.null?"), - "returned buffer freed: {code}" - ); -} - -#[test] -fn list_return_uses_array() { - let api = make_api(vec![simple_module( - "data", - vec![Function { - name: "list_ids".into(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::I32))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - - let code = render(&api, "WeaveFFI", "weaveffi"); - // A list return is one value buffer: count prefix, then elements. - assert!( - code.contains("_wv_value = Array.new(_wv_r.read_len) do"), - "count-driven array: {code}" - ); - assert!( - code.contains("_wv_e0 = _wv_r.read_i32"), - "element decode: {code}" - ); - assert!( - code.contains("_wv_r.expect_end!"), - "trailing bytes rejected: {code}" - ); -} - -#[test] -fn map_return_builds_hash() { - let api = make_api(vec![simple_module( - "data", - vec![Function { - name: "get_metadata".into(), - params: vec![], - returns: Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - - let code = render(&api, "WeaveFFI", "weaveffi"); - // A map return is one value buffer: count, then alternating key/value. - assert!(code.contains("_wv_value = {}"), "hash init: {code}"); - assert!( - code.contains("_wv_r.read_len.times do"), - "count-driven loop: {code}" - ); - assert!( - code.contains("_wv_k0 = _wv_r.read_string") && code.contains("_wv_v0 = _wv_r.read_i32"), - "key/value decode: {code}" - ); - assert!( - code.contains("_wv_value[_wv_k0] = _wv_v0"), - "hash insert: {code}" - ); - // No parallel-array ABI remains. - assert!(!code.contains("out_keys"), "no out_keys: {code}"); - assert!(!code.contains("out_values"), "no out_values: {code}"); -} - -#[test] -fn list_of_strings_return_frees_elements_and_buffer() { - let api = make_api(vec![simple_module( - "data", - vec![plain_fn( - "list_names", - vec![], - Some(TypeRef::List(Box::new(TypeRef::StringUtf8))), - )], - )]); - let code = render(&api, "WeaveFFI", "weaveffi"); - // String elements are decoded from the single value buffer (copies), - // and only that buffer itself is released. - assert!( - code.contains("_wv_e0 = _wv_r.read_string"), - "string elements decoded: {code}" - ); - assert!( - code.contains("weaveffi_free_bytes(result, len) unless result.null?"), - "value buffer freed: {code}" - ); - assert!( - !code.contains("weaveffi_free_string("), - "no per-element frees remain: {code}" - ); -} - -#[test] -fn scalar_list_return_frees_buffer() { - let api = make_api(vec![simple_module( - "data", - vec![plain_fn( - "list_ids", - vec![], - Some(TypeRef::List(Box::new(TypeRef::I32))), - )], - )]); - let code = render(&api, "WeaveFFI", "weaveffi"); - assert!( - code.contains("weaveffi_free_bytes(result, len) unless result.null?"), - "value buffer freed: {code}" - ); -} - -#[test] -fn map_return_decodes_from_one_buffer() { - let api = make_api(vec![simple_module( - "data", - vec![plain_fn( - "get_metadata", - vec![], - Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )), - )], - )]); - let code = render(&api, "WeaveFFI", "weaveffi"); - // Keys and values decode from the single buffer; only it is freed. - assert!( - code.contains("_wv_k0 = _wv_r.read_string"), - "key decode: {code}" - ); - assert!( - code.contains("weaveffi_free_bytes(result, len) unless result.null?"), - "value buffer freed: {code}" - ); - assert!( - !code.contains("keys_ptr") && !code.contains("vals_ptr"), - "no parallel buffers remain: {code}" - ); -} - -#[test] -fn optional_scalar_return_decodes_flag_byte() { - let api = make_api(vec![simple_module( - "data", - vec![plain_fn( - "find_count", - vec![], - Some(TypeRef::Optional(Box::new(TypeRef::I32))), - )], - )]); - let code = render(&api, "WeaveFFI", "weaveffi"); - // An optional scalar is buffered: flag byte, then the value. - assert!(code.contains("if _wv_r.read_flag"), "flag byte: {code}"); - assert!( - code.contains("_wv_value = _wv_r.read_i32"), - "present decode: {code}" - ); - assert!(code.contains("_wv_value = nil"), "absent is nil: {code}"); - assert!( - code.contains("weaveffi_free_bytes(result, len) unless result.null?"), - "value buffer freed: {code}" - ); -} - -#[test] -fn struct_return_wraps_in_class() { - let api = make_api(vec![Module { - name: "data".into(), - functions: vec![Function { - name: "get_item".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I64, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Item".into())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![StructDef { - name: "Item".into(), - doc: None, - fields: vec![StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }]); - - let code = render(&api, "WeaveFFI", "weaveffi"); - // A record return is decoded from its value buffer, which is then - // released with the runtime's free_bytes. - assert!( - code.contains("out_len = FFI::MemoryPointer.new(:size_t)"), - "out_len allocated: {code}" - ); - assert!( - code.contains("_wv_value = _wv_read_item(_wv_r)"), - "record decode: {code}" - ); - assert!( - code.contains("weaveffi_free_bytes(result, len) unless result.null?"), - "value buffer freed: {code}" - ); -} - -#[test] -fn async_function_generates_blocking_wrapper() { - let api = make_api(vec![simple_module( - "io", - vec![Function { - name: "read".into(), - params: vec![], - returns: Some(TypeRef::StringUtf8), - doc: None, - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - - let code = render(&api, "WeaveFFI", "weaveffi"); - // Completion callback type + launcher attach. - assert!( - code.contains("callback :weaveffi_io_read_callback, [:pointer, :pointer, :pointer], :void"), - "async callback decl: {code}" - ); - assert!( - code.contains( - "attach_function :weaveffi_io_read_async, [:weaveffi_io_read_callback, :pointer], :void" - ), - "async launcher attach: {code}" - ); - // Blocking wrapper: trampoline pinned in a local, Queue rendezvous, - // error re-raised on the caller thread. - assert!(code.contains("def self.read()"), "wrapper: {code}"); - assert!(code.contains("queue = Queue.new"), "queue: {code}"); - assert!( - code.contains("callback = FFI::Function.new(:void, [:pointer, :pointer, :pointer])"), - "trampoline: {code}" - ); - assert!( - code.contains("weaveffi_io_read_async(callback, FFI::Pointer::NULL)"), - "launch call: {code}" - ); - assert!(code.contains("value = queue.pop"), "blocking pop: {code}"); - assert!( - code.contains("raise value if value.is_a?(Error)"), - "error re-raise: {code}" - ); - // The generated doc states plainly that the call blocks. - assert!( - code.contains("# Blocks the current thread until the async producer completes"), - "blocking doc: {code}" - ); - // The completion callback copies the owned result string and then - // releases the producer allocation. - assert!( - code.contains("result.read_string"), - "result copied in callback: {code}" - ); - assert!( - code.contains("weaveffi_free_string(result) unless result.null?"), - "owned callback string must be freed after copying: {code}" - ); -} - -#[test] -fn async_bytes_result_copied_then_freed() { - let api = make_api(vec![simple_module( - "io", - vec![Function { - name: "fetch".into(), - params: vec![], - returns: Some(TypeRef::Bytes), - doc: None, - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - let code = render(&api, "WeaveFFI", "weaveffi"); - assert!( - code.contains("result.read_string(result_len)"), - "bytes copied in callback: {code}" - ); - assert!( - code.contains("weaveffi_free_bytes(result, result_len) unless result.null?"), - "owned callback bytes must be freed after copying: {code}" - ); -} - -#[test] -fn iterator_uses_next_destroy_protocol() { - let api = make_api(vec![simple_module( - "events", - vec![Function { - name: "get_messages".into(), - params: vec![], - returns: Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - let code = render(&api, "WeaveFFI", "weaveffi"); - // Launch returns the opaque iterator; next/destroy attached. - assert!( - code.contains("attach_function :weaveffi_events_get_messages, [:pointer], :pointer"), - "launch attach: {code}" - ); - assert!( - code.contains( - "attach_function :weaveffi_events_GetMessagesIterator_next, [:pointer, :pointer, :pointer], :int32" - ), - "next attach: {code}" - ); - assert!( - code.contains( - "attach_function :weaveffi_events_GetMessagesIterator_destroy, [:pointer], :void" - ), - "destroy attach: {code}" - ); - // The wrapper pulls via the iterator protocol, not the list ABI - // (the old lowering wrongly passed an out_len the symbol lacks). - assert!( - code.contains( - "has_item = weaveffi_events_GetMessagesIterator_next(iter, out_item, item_err)" - ), - "pull loop: {code}" - ); - assert!( - code.contains("weaveffi_events_GetMessagesIterator_destroy(iter) unless iter.null?"), - "destroy on disposal: {code}" - ); - assert!(!code.contains("out_len"), "no stray out_len: {code}"); -} - -#[test] -fn iterator_returns_lazy_enumerator_with_ensured_destroy() { - let api = make_api(vec![simple_module( - "events", - vec![Function { - name: "get_messages".into(), - params: vec![], - returns: Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - let code = render(&api, "WeaveFFI", "weaveffi"); - let body = code - .split("def self.get_messages()") - .nth(1) - .expect("wrapper body"); - let body = body.split("\n end\n").next().expect("wrapper body end"); - // Lazy Enumerator, never a hidden drain into an Array. - assert!( - body.contains("Enumerator.new do |y|"), - "lazy Enumerator: {code}" - ); - assert!(!body.contains("items = []"), "no eager drain: {code}"); - assert!(!body.contains(".to_a"), "no hidden collect: {code}"); - // The launch happens inside the block, so an unstarted enumerator - // never acquires (and thus can never leak) a handle. - let launch = body - .find("iter = weaveffi_events_get_messages(err)") - .expect("launch"); - let enum_open = body.find("Enumerator.new do |y|").expect("enumerator"); - assert!(enum_open < launch, "launch inside enumerator block: {code}"); - // Destroy runs from an ensure block, guarding early break, and each - // yielded string is freed after copying. - let ensure_pos = body.find("ensure").expect("ensure block"); - let destroy_pos = body - .find("weaveffi_events_GetMessagesIterator_destroy(iter)") - .expect("destroy call"); - assert!(ensure_pos < destroy_pos, "destroy inside ensure: {code}"); - assert!( - body.contains("weaveffi_free_string(item_ptr)"), - "yielded string freed after copy: {code}" - ); - assert!(body.contains("y << item"), "yields through yielder: {code}"); - // The generated docs describe the lazy contract. - assert!( - code.contains("# Returns a lazy Enumerator"), - "doc states Enumerator return: {code}" - ); -} - -#[test] -fn iterator_of_records_adopts_each_element() { - let api = make_api(vec![Module { - name: "kv".into(), - functions: vec![Function { - name: "scan_entries".into(), - params: vec![], - returns: Some(TypeRef::Iterator(Box::new(TypeRef::Record("Entry".into())))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![StructDef { - name: "Entry".into(), - doc: None, - fields: vec![StructField { - name: "key".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }]); - let code = render(&api, "WeaveFFI", "weaveffi"); - // Each yielded record arrives as a producer-allocated value buffer: - // the wrapper copies the bytes, frees them, then decodes and yields. - assert!( - code.contains("out_item_len = FFI::MemoryPointer.new(:size_t)"), - "element length out-param: {code}" - ); - assert!( - code.contains("weaveffi_free_bytes(item_ptr, item_len) unless item_ptr.null?"), - "element buffer freed: {code}" - ); - assert!( - code.contains("_wv_item = _wv_read_entry(_wv_r)"), - "element decoded: {code}" - ); - assert!(code.contains("y << _wv_item"), "decoded yield: {code}"); - assert!( - code.contains("Enumerator.new do |y|"), - "record iterator is lazy: {code}" - ); -} - -#[test] -fn interface_iterator_method_is_lazy_and_qualified() { - let mut m = simple_module("kv", vec![]); - m.interfaces = vec![InterfaceDef { - name: "Store".into(), - doc: None, - constructors: vec![plain_fn("new", vec![], None)], - methods: vec![plain_fn( - "keys", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - )], - statics: vec![], - }]; - let code = render(&make_api(vec![m]), "WeaveFFI", "weaveffi"); - let body = code.split("def keys()").nth(1).expect("keys wrapper"); - assert!( - body.contains("Enumerator.new do |y|"), - "method iterator is lazy: {code}" - ); - assert!( - body.contains("iter = WeaveFFI.weaveffi_kv_Store_keys(@handle, err)"), - "launch passes self and qualifies: {code}" - ); - assert!( - body.contains("WeaveFFI.weaveffi_kv_Store_KeysIterator_destroy(iter) unless iter.null?"), - "qualified ensure destroy: {code}" - ); -} - -#[test] -fn listener_register_unregister_wrappers() { - use weaveffi_ir::ir::{CallbackDef, ListenerDef}; - let api = make_api(vec![Module { - callbacks: vec![CallbackDef { - name: "OnMessage".into(), - params: vec![Param { - name: "message".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - doc: None, - }], - listeners: vec![ListenerDef { - name: "message_listener".into(), - event_callback: "OnMessage".into(), - doc: None, - }], - ..simple_module("events", vec![]) - }]); - let code = render(&api, "WeaveFFI", "weaveffi"); - assert!( - code.contains("callback :weaveffi_events_OnMessage_fn, [:string, :pointer], :void"), - "callback decl: {code}" - ); - assert!( - code.contains( - "attach_function :weaveffi_events_register_message_listener, [:weaveffi_events_OnMessage_fn, :pointer], :uint64" - ), - "register attach: {code}" - ); - assert!( - code.contains("def self.register_message_listener(&block)"), - "register wrapper: {code}" - ); - assert!( - code.contains("@listener_refs[listener_id] = trampoline"), - "trampoline pinned: {code}" - ); - assert!( - code.contains("def self.unregister_message_listener(listener_id)"), - "unregister wrapper: {code}" - ); - assert!( - code.contains("@listener_refs.delete(listener_id)"), - "trampoline released: {code}" - ); -} - -#[test] -fn preamble_has_platform_detection() { - let code = render(&make_api(vec![]), "WeaveFFI", "weaveffi"); - assert!(code.contains("FFI::Platform::OS"), "platform: {code}"); - assert!(code.contains("libweaveffi.dylib"), "darwin: {code}"); - assert!(code.contains("weaveffi.dll"), "windows: {code}"); - assert!(code.contains("libweaveffi.so"), "linux: {code}"); -} - -#[test] -fn error_class_structure() { - let code = render(&make_api(vec![]), "WeaveFFI", "weaveffi"); - assert!( - code.contains("class Error < StandardError"), - "Error class: {code}" - ); - assert!(code.contains("attr_reader :code"), "code attr: {code}"); - // The error struct layout carries the structured payload slots. - assert!( - code.contains(":payload_ptr, :pointer") && code.contains(":payload_len, :size_t"), - "payload slots in ErrorStruct: {code}" - ); -} - -#[test] -fn preamble_has_buffer_runtime() { - let code = render(&make_api(vec![]), "WeaveFFI", "weaveffi"); - assert!( - code.contains("class WvBufferWriter"), - "buffer writer: {code}" - ); - assert!( - code.contains("class WvBufferReader"), - "buffer reader: {code}" - ); - // Little-endian packed directives and strict decoding guards. - assert!(code.contains("[v].pack('l<')"), "LE i32 pack: {code}"); - assert!(code.contains("unpack1('E')"), "f64 unpack: {code}"); - assert!( - code.contains("'malformed value buffer: trailing bytes after value'"), - "trailing byte guard: {code}" - ); - assert!( - code.contains("'malformed value buffer: string is not valid UTF-8'"), - "UTF-8 guard: {code}" - ); - assert!( - code.contains("'malformed value buffer: length prefix exceeds remaining bytes'"), - "length guard: {code}" - ); -} - -#[test] -fn handle_type_uses_uint64() { - let api = make_api(vec![simple_module( - "store", - vec![Function { - name: "create".into(), - params: vec![], - returns: Some(TypeRef::Handle), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - - let code = render(&api, "WeaveFFI", "weaveffi"); - assert!(code.contains(":uint64"), "handle type: {code}"); -} - -#[test] -fn ffi_type_mapping() { - let types = |ty: &TypeRef| rb_abi_types(&abi::lower_param("_", ty, "", false), false); - assert_eq!(types(&TypeRef::I32), vec![":int32"]); - assert_eq!(types(&TypeRef::U32), vec![":uint32"]); - assert_eq!(types(&TypeRef::I64), vec![":int64"]); - assert_eq!(types(&TypeRef::F64), vec![":double"]); - assert_eq!(types(&TypeRef::Bool), vec![":int32"]); - assert_eq!(types(&TypeRef::Handle), vec![":uint64"]); - assert_eq!(types(&TypeRef::StringUtf8), vec![":string"]); - assert_eq!(types(&TypeRef::Enum("Color".into())), vec![":int32"]); - // Buffered types lower to a (ptr, len) slot pair. - assert_eq!( - types(&TypeRef::Record("Foo".into())), - vec![":pointer", ":size_t"] - ); - assert_eq!( - types(&TypeRef::List(Box::new(TypeRef::I32))), - vec![":pointer", ":size_t"] - ); -} - -#[test] -fn return_type_string_is_pointer() { - let ret = abi::lower_return(&TypeRef::StringUtf8, ""); - assert_eq!(rb_ffi_type(&ret.ret, true), ":pointer"); -} - -#[test] -fn return_type_map_is_buffer_with_out_len() { - let ret = abi::lower_return( - &TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - "", - ); - assert_eq!(rb_ffi_type(&ret.ret, true), ":pointer"); - let out: Vec<_> = ret - .out_params - .iter() - .map(|p| rb_ffi_type(&p.ty, true)) - .collect(); - assert_eq!(out, vec![":pointer"]); -} - -#[test] -fn enum_param_passes_int32() { - let api = make_api(vec![simple_module( - "gfx", - vec![Function { - name: "set_color".into(), - params: vec![Param { - name: "color".into(), - ty: TypeRef::Enum("Color".into()), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - - let code = render(&api, "WeaveFFI", "weaveffi"); - assert!(code.contains(":int32"), "enum type: {code}"); -} - -#[test] -fn void_function_no_result() { - let api = make_api(vec![simple_module( - "store", - vec![Function { - name: "clear".into(), - params: vec![], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - - let code = render(&api, "WeaveFFI", "weaveffi"); - assert!(code.contains(":void"), "void return: {code}"); - assert!( - !code.contains("result = weaveffi_store_clear"), - "no result capture: {code}" - ); -} - -#[test] -fn list_of_structs_return() { - let api = make_api(vec![Module { - name: "data".into(), - functions: vec![Function { - name: "list_items".into(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::Record("Item".into())))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![StructDef { - name: "Item".into(), - doc: None, - fields: vec![StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }]); - - let code = render(&api, "WeaveFFI", "weaveffi"); - // Record list elements decode recursively through the record codec. - assert!( - code.contains("_wv_e0 = _wv_read_item(_wv_r)"), - "struct list element: {code}" - ); - assert!( - code.contains("_wv_value = Array.new(_wv_r.read_len) do"), - "count-driven array: {code}" - ); -} - -#[test] -fn optional_struct_returns_nil_on_null() { - let api = make_api(vec![simple_module( - "data", - vec![Function { - name: "find_item".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I64, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Optional(Box::new(TypeRef::Record("Item".into())))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - - let code = render(&api, "WeaveFFI", "weaveffi"); - // An optional record is buffered: the flag byte selects nil or a - // decoded value class instance. - assert!(code.contains("if _wv_r.read_flag"), "flag byte: {code}"); - assert!( - code.contains("_wv_value = _wv_read_item(_wv_r)"), - "present decode: {code}" - ); - assert!(code.contains("_wv_value = nil"), "absent is nil: {code}"); -} - -// ── Comprehensive tests ── - -fn contacts_api() -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules: vec![Module { - name: "contacts".into(), - functions: vec![ - Function { - name: "create_contact".into(), - params: vec![ - Param { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }, - Param { - name: "last_name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }, - Param { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - mutable: false, - doc: None, - }, - Param { - name: "contact_type".into(), - ty: TypeRef::Enum("ContactType".into()), - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::Handle), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "get_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Handle, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "list_contacts".into(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::Record("Contact".into())))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "delete_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Handle, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Bool), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "count_contacts".into(), - params: vec![], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - interfaces: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: None, - }, - StructField { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "last_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "email".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - doc: None, - }, - StructField { - name: "contact_type".into(), - ty: TypeRef::Enum("ContactType".into()), - doc: None, - }, - ], - }], - enums: vec![EnumDef { - name: "ContactType".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Personal".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Work".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Other".into(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }], - generators: None, - package: None, - }) -} - -#[test] -fn generate_ruby_basic() { - let api = make_api(vec![simple_module( - "math", - vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - - let tmp = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(tmp.path()).expect("valid UTF-8"); - - RubyGenerator - .generate(&api, out_dir, &RubyConfig::default()) - .unwrap(); - - let rb = std::fs::read_to_string(tmp.path().join("ruby/lib/weaveffi.rb")).unwrap(); - assert!(rb.contains("module WeaveFFI"), "module name: {rb}"); - assert!( - rb.contains("attach_function :weaveffi_math_add"), - "attach_function: {rb}" - ); - assert!(rb.contains("def self.add(a, b)"), "wrapper fn: {rb}"); - assert!(rb.contains("check_error!(err)"), "error check: {rb}"); -} - -#[test] -fn generate_ruby_with_structs() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "get_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::Handle, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - StructField { - name: "first_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "last_name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }]); - - let tmp = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(tmp.path()).expect("valid UTF-8"); - - RubyGenerator - .generate(&api, out_dir, &RubyConfig::default()) - .unwrap(); - - let rb = std::fs::read_to_string(tmp.path().join("ruby/lib/weaveffi.rb")).unwrap(); - assert!(rb.contains("class Contact"), "struct class: {rb}"); - assert!( - rb.contains("attr_reader :first_name"), - "first_name attr: {rb}" - ); - assert!( - rb.contains("attr_reader :last_name"), - "last_name attr: {rb}" - ); - assert!( - rb.contains("def initialize(first_name:, last_name:)"), - "kwargs initialize: {rb}" - ); - assert!( - rb.contains("_wv_value = _wv_read_contact(_wv_r)"), - "struct return decode: {rb}" - ); - assert!( - !rb.contains("FFI::AutoPointer"), - "no pointer wrapping remains: {rb}" - ); -} - -#[test] -fn generate_ruby_with_enums() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "classify".into(), - params: vec![Param { - name: "ct".into(), - ty: TypeRef::Enum("ContactType".into()), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Enum("ContactType".into())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![EnumDef { - name: "ContactType".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Personal".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Work".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Other".into(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }]); - - let tmp = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(tmp.path()).expect("valid UTF-8"); - - RubyGenerator - .generate(&api, out_dir, &RubyConfig::default()) - .unwrap(); - - let rb = std::fs::read_to_string(tmp.path().join("ruby/lib/weaveffi.rb")).unwrap(); - assert!(rb.contains("module ContactType"), "enum module: {rb}"); - assert!(rb.contains("PERSONAL = 0"), "variant 0: {rb}"); - assert!(rb.contains("WORK = 1"), "variant 1: {rb}"); - assert!(rb.contains("OTHER = 2"), "variant 2: {rb}"); - assert!(rb.contains(":int32"), "enum ffi type: {rb}"); -} - -#[test] -fn generate_ruby_with_optionals() { - let api = make_api(vec![simple_module( - "data", - vec![ - Function { - name: "find_name".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I64, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "find_count".into(), - params: vec![Param { - name: "key".into(), - ty: TypeRef::Optional(Box::new(TypeRef::I32)), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Optional(Box::new(TypeRef::I32))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - )]); - - let tmp = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(tmp.path()).expect("valid UTF-8"); - - RubyGenerator - .generate(&api, out_dir, &RubyConfig::default()) - .unwrap(); - - let rb = std::fs::read_to_string(tmp.path().join("ruby/lib/weaveffi.rb")).unwrap(); - // Optional returns decode a flag byte from the value buffer. - assert!( - rb.contains("if _wv_r.read_flag"), - "flag byte on optional return: {rb}" - ); - assert!(rb.contains("_wv_value = nil"), "absent is nil: {rb}"); - // An optional parameter packs a flag byte (plus the value when - // present) into the value buffer handed to the C call. - assert!( - rb.contains("key_w = WvBufferWriter.new"), - "optional param writer: {rb}" - ); - assert!( - rb.contains("key_w.write_flag(false)") && rb.contains("key_w.write_flag(true)"), - "optional param flag: {rb}" - ); - assert!( - rb.contains("key_w.write_i32(key)"), - "optional param value: {rb}" - ); - assert!( - rb.contains("key_buf, key_data.bytesize"), - "optional param slot pair: {rb}" - ); -} - -#[test] -fn generate_ruby_with_lists() { - let api = make_api(vec![simple_module( - "data", - vec![ - Function { - name: "list_ids".into(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::I32))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "set_names".into(), - params: vec![Param { - name: "names".into(), - ty: TypeRef::List(Box::new(TypeRef::StringUtf8)), - mutable: false, - doc: None, - }], - returns: None, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - )]); - - let tmp = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(tmp.path()).expect("valid UTF-8"); - - RubyGenerator - .generate(&api, out_dir, &RubyConfig::default()) - .unwrap(); - - let rb = std::fs::read_to_string(tmp.path().join("ruby/lib/weaveffi.rb")).unwrap(); - // List returns decode count-prefixed elements from the value buffer. - assert!( - rb.contains("_wv_value = Array.new(_wv_r.read_len) do"), - "list return decode: {rb}" - ); - // A list parameter packs count then elements, and hands the C call a - // MemoryPointer copy of the encoding. - assert!( - rb.contains("names_w.write_len(names.length)"), - "list param count: {rb}" - ); - assert!( - rb.contains("names.each do |_wv_e0|"), - "list param elements: {rb}" - ); - assert!( - rb.contains("names_w.write_string(_wv_e0)"), - "list param element write: {rb}" - ); - assert!( - rb.contains("names_buf = FFI::MemoryPointer.new(:uint8, names_data.bytesize)"), - "list param buffer copy: {rb}" - ); - assert!( - rb.contains("names_buf, names_data.bytesize"), - "list param slot pair: {rb}" - ); -} - -#[test] -fn generate_ruby_full_contacts() { - let api = contacts_api(); - - let tmp = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(tmp.path()).expect("valid UTF-8"); - - RubyGenerator - .generate(&api, out_dir, &RubyConfig::default()) - .unwrap(); - - let rb = std::fs::read_to_string(tmp.path().join("ruby/lib/weaveffi.rb")).unwrap(); - - assert!(rb.contains("module WeaveFFI"), "module: {rb}"); - assert!(rb.contains("module ContactType"), "enum: {rb}"); - assert!(rb.contains("PERSONAL = 0"), "enum variant: {rb}"); - assert!(rb.contains("class Contact"), "struct class: {rb}"); - assert!( - rb.contains("def self.create_contact(first_name, last_name, email, contact_type)"), - "create fn: {rb}" - ); - assert!(rb.contains("def self.get_contact(id)"), "get fn: {rb}"); - assert!(rb.contains("def self.list_contacts"), "list fn: {rb}"); - assert!( - rb.contains("def self.delete_contact(id)"), - "delete fn: {rb}" - ); - assert!(rb.contains("def self.count_contacts"), "count fn: {rb}"); - assert!(rb.contains("attr_reader :id"), "id attr: {rb}"); - assert!( - rb.contains("attr_reader :first_name"), - "first_name attr: {rb}" - ); - assert!(rb.contains("attr_reader :email"), "email attr: {rb}"); - assert!( - rb.contains("attr_reader :contact_type"), - "contact_type attr: {rb}" - ); - - let gemspec = std::fs::read_to_string(tmp.path().join("ruby/weaveffi.gemspec")).unwrap(); - assert!( - gemspec.contains("s.name = 'weaveffi'"), - "gem name: {gemspec}" - ); - - let readme = std::fs::read_to_string(tmp.path().join("ruby/README.md")).unwrap(); - assert!(readme.contains("Ruby"), "readme: {readme}"); -} - -#[test] -fn ruby_custom_module_name() { - let api = make_api(vec![simple_module( - "math", - vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - - let tmp = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(tmp.path()).expect("valid UTF-8"); - - let config = RubyConfig { - module_name: Some("MyBindings".into()), - gem_name: Some("my_bindings".into()), - ..RubyConfig::default() - }; - RubyGenerator.generate(&api, out_dir, &config).unwrap(); - - let rb = std::fs::read_to_string(tmp.path().join("ruby/lib/my_bindings.rb")).unwrap(); - assert!(rb.contains("module MyBindings"), "custom module name: {rb}"); - assert!( - !rb.contains("module WeaveFFI"), - "should not contain default module name: {rb}" - ); - - let gemspec = std::fs::read_to_string(tmp.path().join("ruby/my_bindings.gemspec")).unwrap(); - assert!( - gemspec.contains("s.name = 'my_bindings'"), - "custom gem name: {gemspec}" - ); - assert!( - !gemspec.contains("s.name = 'weaveffi'"), - "should not contain default gem name: {gemspec}" - ); -} - -#[test] -fn ruby_no_double_free_on_error() { - let api = make_api(vec![Module { - name: "contacts".into(), - interfaces: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - functions: vec![Function { - name: "find_contact".into(), - params: vec![Param { - name: "name".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Record("Contact".into())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - errors: None, - modules: vec![], - }]); - - let rb = render(&api, "WeaveFFI", "weaveffi"); - - let fn_start = rb - .find("def self.find_contact(name)") - .expect("find_contact wrapper"); - let fn_body = &rb[fn_start..]; - let fn_end = fn_body.find("\n end\n").unwrap(); - let fn_text = &fn_body[..fn_end]; - - assert!( - !fn_text.contains("weaveffi_free_string(name"), - "borrowed string param must not be freed by wrapper: {fn_text}" - ); - - let err_check = fn_text - .find("check_error!(err)") - .expect("check_error in find_contact"); - let buffer_free = fn_text - .find("weaveffi_free_bytes(result, len)") - .expect("free_bytes in find_contact"); - let decode = fn_text - .find("_wv_read_contact(_wv_r)") - .expect("decode in find_contact"); - assert!( - err_check < buffer_free, - "error must be checked before touching the result buffer: {fn_text}" - ); - assert!( - buffer_free < decode, - "buffer is copied and freed exactly once before decoding: {fn_text}" - ); - assert_eq!( - fn_text.matches("weaveffi_free_bytes(result").count(), - 1, - "result buffer freed exactly once: {fn_text}" - ); -} - -#[test] -fn ruby_null_check_on_optional_return() { - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![Function { - name: "find_contact".into(), - params: vec![Param { - name: "id".into(), - ty: TypeRef::I64, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::Optional(Box::new(TypeRef::Record( - "Contact".into(), - )))), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }]); - - let rb = render(&api, "WeaveFFI", "weaveffi"); - - let fn_start = rb - .find("def self.find_contact(id)") - .expect("find_contact wrapper"); - let fn_body = &rb[fn_start..]; - let fn_end = fn_body.find("\n end\n").unwrap(); - let fn_text = &fn_body[..fn_end]; - - // The flag byte gates decoding: the record codec only runs for a - // present value, and an absent one yields nil. - let flag_check = fn_text - .find("if _wv_r.read_flag") - .expect("flag check in find_contact"); - let contact_decode = fn_text - .find("_wv_read_contact(_wv_r)") - .expect("decode in find_contact"); - assert!( - flag_check < contact_decode, - "optional record return should check the flag before decoding: {fn_text}" - ); - assert!( - fn_text.contains("_wv_value = nil"), - "absent optional is nil: {fn_text}" - ); -} - -fn doc_api() -> ResolvedApi { - make_api(vec![Module { - name: "docs".into(), - functions: vec![Function { - name: "do_thing".into(), - params: vec![Param { - name: "x".into(), - ty: TypeRef::I32, - mutable: false, - doc: Some("the input value".into()), - }], - returns: Some(TypeRef::I32), - doc: Some("Performs a thing.".into()), - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - interfaces: vec![], - structs: vec![StructDef { - name: "Item".into(), - doc: Some("An item we track.".into()), - fields: vec![StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: Some("Stable id".into()), - }], - }], - enums: vec![EnumDef { - name: "Kind".into(), - doc: Some("Kind of item.".into()), - variants: vec![EnumVariant { - name: "Small".into(), - value: 0, - doc: Some("A small one".into()), - fields: vec![], - }], - }], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }]) -} - -#[test] -fn ruby_emits_doc_on_function() { - let rb = render(&doc_api(), "Weaveffi", "weaveffi"); - assert!(rb.contains("# Performs a thing."), "{rb}"); -} - -#[test] -fn ruby_emits_doc_on_struct() { - let rb = render(&doc_api(), "Weaveffi", "weaveffi"); - assert!(rb.contains("# An item we track."), "{rb}"); -} - -#[test] -fn ruby_emits_doc_on_enum_variant() { - let rb = render(&doc_api(), "Weaveffi", "weaveffi"); - assert!(rb.contains("# Kind of item."), "{rb}"); - assert!(rb.contains("# A small one"), "{rb}"); -} - -#[test] -fn ruby_emits_doc_on_field() { - let rb = render(&doc_api(), "Weaveffi", "weaveffi"); - assert!(rb.contains("# Stable id"), "{rb}"); -} - -#[test] -fn ruby_emits_doc_on_param() { - let rb = render(&doc_api(), "Weaveffi", "weaveffi"); - assert!(rb.contains("# @param x [Object] the input value"), "{rb}"); -} - -#[test] -fn ruby_custom_prefix_threads_to_user_symbols() { - let api = make_api(vec![simple_module( - "math", - vec![Function { - name: "add".into(), - params: vec![ - Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - Param { - name: "b".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - )]); - - let code = render(&api, "WeaveFFI", "myffi"); - - assert!( - code.contains("attach_function :myffi_math_add"), - "user symbol should adopt custom prefix: {code}" - ); - assert!( - !code.contains("weaveffi_math_add"), - "user symbol must not retain default prefix: {code}" - ); - assert!( - code.contains("weaveffi_error_clear"), - "runtime ABI helper must stay literal: {code}" - ); -} - -fn shapes_api() -> ResolvedApi { - make_api(vec![Module { - name: "shapes".into(), - functions: vec![ - Function { - name: "describe".into(), - params: vec![Param { - name: "shape".into(), - ty: TypeRef::RichEnum("Shape".into()), - mutable: false, - doc: None, - }], - returns: Some(TypeRef::StringUtf8), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - Function { - name: "scale".into(), - params: vec![ - Param { - name: "shape".into(), - ty: TypeRef::RichEnum("Shape".into()), - mutable: false, - doc: None, - }, - Param { - name: "factor".into(), - ty: TypeRef::F64, - mutable: false, - doc: None, - }, - ], - returns: Some(TypeRef::RichEnum("Shape".into())), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - ], - interfaces: vec![], - structs: vec![], - enums: vec![ - EnumDef { - name: "Shape".into(), - doc: Some("An algebraic shape".into()), - variants: vec![ - EnumVariant { - name: "Empty".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Circle".into(), - value: 1, - doc: None, - fields: vec![StructField { - name: "radius".into(), - ty: TypeRef::F64, - doc: None, - }], - }, - EnumVariant { - name: "Rectangle".into(), - value: 2, - doc: None, - fields: vec![ - StructField { - name: "width".into(), - ty: TypeRef::F32, - doc: None, - }, - StructField { - name: "height".into(), - ty: TypeRef::F32, - doc: None, - }, - ], - }, - EnumVariant { - name: "Labeled".into(), - value: 3, - doc: None, - fields: vec![ - StructField { - name: "label".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "count".into(), - ty: TypeRef::U8, - doc: None, - }, - ], - }, - ], - }, - EnumDef { - name: "Channel".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }, - ], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }]) -} - -#[test] -fn rich_enum_renders_tagged_class_hierarchy() { - let code = render(&shapes_api(), "Shapes", "weaveffi"); - - // A rich enum is a tagged class hierarchy, never a plain constants - // module and never an opaque pointer wrapper. - assert!( - !code.contains("module Shape\n"), - "rich enum must not be a plain enum module: {code}" - ); - assert!(code.contains("class Shape\n"), "rich enum base: {code}"); - assert!( - !code.contains("ShapePtr") && !code.contains("FFI::AutoPointer"), - "no pointer wrapper: {code}" - ); - - // One subclass per variant, carrying its TAG and fields. - assert!( - code.contains("class Empty < Shape") && code.contains("TAG = 0"), - "unit variant: {code}" - ); - assert!( - code.contains("class Circle < Shape") && code.contains("TAG = 1"), - "circle variant: {code}" - ); - assert!( - code.contains("class Labeled < Shape") && code.contains("TAG = 3"), - "labeled variant: {code}" - ); - assert!(code.contains("attr_reader :radius"), "circle field: {code}"); - assert!( - code.contains("def initialize(radius:)"), - "circle kwargs initialize: {code}" - ); - assert!( - code.contains("def initialize(width:, height:)"), - "rectangle kwargs initialize: {code}" - ); - assert!( - code.contains("def tag") && code.contains("self.class::TAG"), - "tag reader: {code}" - ); - assert!(code.contains("def ==(other)"), "structural eq: {code}"); - - // Rich enums own no C symbols at all. - assert!( - !code.contains("attach_function :weaveffi_shapes_Shape"), - "no rich enum C symbols: {code}" - ); - - // Plain sibling enum still renders as a constants module. - assert!( - code.contains("module Channel"), - "plain enum still a module: {code}" - ); -} - -#[test] -fn rich_enum_codec_and_wrappers_use_value_buffers() { - let code = render(&shapes_api(), "Shapes", "weaveffi"); - - // The pack helper dispatches on the variant class and writes the tag - // followed by the variant's fields; unknown objects trap. - assert!( - code.contains("def self._wv_write_shape(w, v)"), - "pack helper: {code}" - ); - assert!( - code.contains("when Shape::Circle"), - "variant dispatch: {code}" - ); - let circle_pack = code - .split("when Shape::Circle") - .nth(1) - .expect("circle pack arm"); - assert!( - circle_pack.contains("w.write_i32(1)") && circle_pack.contains("w.write_f64(v.radius)"), - "tag then fields: {code}" - ); - assert!( - code.contains("raise Error.new(-1, 'unknown Shape variant')"), - "unknown variant trap: {code}" - ); - - // The unpack helper switches on the decoded tag and constructs the - // matching subclass; unknown tags trap. - assert!( - code.contains("def self._wv_read_shape(r)"), - "unpack helper: {code}" - ); - assert!(code.contains("tag = r.read_i32"), "tag decode: {code}"); - assert!( - code.contains("Shape::Circle.new(radius: _wv_radius)"), - "circle construction: {code}" - ); - assert!( - code.contains("Shape::Rectangle.new(width: _wv_width, height: _wv_height)"), - "rectangle construction: {code}" - ); - assert!( - code.contains("Shape::Empty.new"), - "unit construction: {code}" - ); - - // A rich enum parameter packs into a value buffer and passes the - // (ptr, len) slot pair; a rich enum return decodes from one. - assert!( - code.contains("def self.describe(shape)"), - "describe wrapper: {code}" - ); - assert!( - code.contains("_wv_write_shape(shape_w, shape)"), - "describe packs param: {code}" - ); - assert!( - code.contains("shape_buf, shape_data.bytesize"), - "describe slot pair: {code}" - ); - assert!( - code.contains("_wv_value = _wv_read_shape(_wv_r)"), - "scale decodes return: {code}" - ); -} - -/// A mixed module exercising every buffered surface at once: records -/// nested in rich enums, buffered parameters and returns at module and -/// interface scope, a typed error with payload fields, a buffered async -/// result, a buffered iterator element, and a buffered listener argument. -#[test] -fn kitchen_sink_module_renders_coherently() { - let mut m = simple_module( - "kv", - vec![ - { - let mut f = plain_fn( - "kv_lookup", - vec![str_param("key")], - Some(TypeRef::Record("Entry".into())), - ); - f.throws = true; - f - }, - plain_fn( - "kv_tags", - vec![Param { - name: "filter".into(), - ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)), - mutable: false, - doc: None, - }], - Some(TypeRef::List(Box::new(TypeRef::StringUtf8))), - ), - plain_fn( - "kv_meta", - vec![Param { - name: "entries".into(), - ty: TypeRef::List(Box::new(TypeRef::Record("Entry".into()))), - mutable: false, - doc: None, - }], - Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )), - ), - { - let mut f = plain_fn( - "kv_load", - vec![], - Some(TypeRef::List(Box::new(TypeRef::RichEnum("Event".into())))), - ); - f.r#async = true; - f.throws = true; - f - }, - { - let mut f = plain_fn( - "kv_scan", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::Record("Entry".into())))), - ); - f.throws = true; - f - }, - ], - ); - m.structs = vec![StructDef { - name: "Entry".into(), - doc: None, - fields: vec![ - StructField { - name: "key".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "hits".into(), - ty: TypeRef::Optional(Box::new(TypeRef::U32)), - doc: None, - }, - ], - }]; - m.enums = vec![EnumDef { - name: "Event".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Added".into(), - value: 0, - doc: None, - fields: vec![StructField { - name: "entry".into(), - ty: TypeRef::Record("Entry".into()), - doc: None, - }], - }, - EnumVariant { - name: "Cleared".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }]; - m.errors = Some(ErrorDomain { - name: "KvError".into(), - codes: vec![ErrorCode { - name: "KeyNotFound".into(), - code: 1001, - message: "key not found".into(), - doc: None, - fields: vec![StructField { - name: "key".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - }); - m.interfaces = vec![InterfaceDef { - name: "Store".into(), - doc: None, - constructors: vec![plain_fn("new", vec![str_param("path")], None)], - methods: vec![plain_fn( - "put", - vec![Param { - name: "entry".into(), - ty: TypeRef::Record("Entry".into()), - mutable: false, - doc: None, - }], - None, - )], - statics: vec![], - }]; - use weaveffi_ir::ir::{CallbackDef, ListenerDef}; - m.callbacks = vec![CallbackDef { - name: "OnEvent".into(), - params: vec![Param { - name: "event".into(), - ty: TypeRef::RichEnum("Event".into()), - mutable: false, - doc: None, - }], - doc: None, - }]; - m.listeners = vec![ListenerDef { - name: "event_listener".into(), - event_callback: "OnEvent".into(), - doc: None, - }]; - let code = render(&make_api(vec![m]), "WeaveFFI", "weaveffi"); - // Codec calls inside a class body qualify the module receiver; at - // module scope they stay bare. - assert!( - code.contains("WeaveFFI._wv_write_entry(entry_w, entry)"), - "qualified codec call in interface method: {code}" - ); - assert!( - code.contains("_wv_write_entry(entries_w, _wv_e0)"), - "list element pack at module scope: {code}" - ); - // The rich enum codec recurses into the record codec for its - // record-typed variant field. - assert!( - code.contains("_wv_entry = _wv_read_entry(r)"), - "nested record decode in rich enum codec: {code}" - ); - // The async list-of-rich-enum result decodes elementwise inside the - // completion callback. - assert!( - code.contains("_wv_e0 = _wv_read_event(_wv_r)"), - "async rich enum element decode: {code}" - ); - // The iterator's buffered elements route through the record codec. - assert!( - code.contains("_wv_item = _wv_read_entry(_wv_r)"), - "iterator element decode: {code}" - ); - // The listener decodes the borrowed rich enum before the dispatch. - assert!( - code.contains("event_v = _wv_read_event(event_r)"), - "listener rich enum decode: {code}" - ); -} - -#[test] -fn async_buffered_result_decoded_inside_callback() { - let api = make_api(vec![Module { - name: "io".into(), - functions: vec![{ - let mut f = plain_fn( - "load_tags", - vec![], - Some(TypeRef::List(Box::new(TypeRef::StringUtf8))), - ); - f.r#async = true; - f - }], - interfaces: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }]); - let code = render(&api, "WeaveFFI", "weaveffi"); - // The owned result buffer is copied, freed, and decoded inside the - // callback. - assert!( - code.contains( - "_wv_r = WvBufferReader.new(result_ptr.null? ? ''.b : result_ptr.read_string(result_len))" - ), - "owned buffer copied and decoded: {code}" - ); - assert!(code.contains("queue << _wv_v"), "decoded push: {code}"); - assert!( - code.contains("weaveffi_free_bytes(result_ptr, result_len) unless result_ptr.null?"), - "owned callback buffer must be freed after copying: {code}" - ); - // A decode failure surfaces through the queue rather than raising - // across the C callback boundary. - assert!( - code.contains("rescue Error => e") && code.contains("queue << e"), - "decode errors queued: {code}" - ); -} - -#[test] -fn listener_buffered_argument_decoded_before_dispatch() { - use weaveffi_ir::ir::{CallbackDef, ListenerDef}; - let api = make_api(vec![Module { - callbacks: vec![CallbackDef { - name: "OnUpdate".into(), - params: vec![Param { - name: "entry".into(), - ty: TypeRef::Record("Entry".into()), - mutable: false, - doc: None, - }], - doc: None, - }], - listeners: vec![ListenerDef { - name: "update_listener".into(), - event_callback: "OnUpdate".into(), - doc: None, - }], - structs: vec![StructDef { - name: "Entry".into(), - doc: None, - fields: vec![StructField { - name: "key".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - ..simple_module("events", vec![]) - }]); - let code = render(&api, "WeaveFFI", "weaveffi"); - // The callback type declares the borrowed (ptr, len) slot pair. - assert!( - code.contains( - "callback :weaveffi_events_OnUpdate_fn, [:pointer, :size_t, :pointer], :void" - ), - "callback decl: {code}" - ); - // The trampoline decodes the borrowed buffer before the dispatch and - // hands the block the decoded value. - assert!( - code.contains( - "entry_r = WvBufferReader.new(entry_ptr.null? ? ''.b : entry_ptr.read_string(entry_len))" - ), - "borrowed arg decoded: {code}" - ); - assert!( - code.contains("_wv_entry_v = _wv_read_entry(entry_r)") - || code.contains("entry_v = _wv_read_entry(entry_r)"), - "record decode: {code}" - ); - assert!(code.contains("block.call(entry_v)"), "dispatch: {code}"); - // A malformed buffer drops the event instead of raising across the - // C callback boundary. - assert!( - code.contains("warn \"weaveffi: dropped OnUpdate event: #{e.message}\""), - "malformed event dropped: {code}" - ); -} - -#[test] -fn keyword_params_and_fields_gain_trailing_underscore() { - let api = make_api(vec![Module { - structs: vec![StructDef { - name: "Config".into(), - doc: None, - fields: vec![ - StructField { - name: "end".into(), - ty: TypeRef::StringUtf8, - doc: None, - }, - StructField { - name: "normal".into(), - ty: TypeRef::Bool, - doc: None, - }, - ], - }], - ..simple_module( - "kw", - vec![plain_fn( - "load", - vec![str_param("end"), str_param("in"), str_param("key")], - Some(TypeRef::I32), - )], - ) - }]); - let code = render(&api, "WeaveFFI", "weaveffi"); - // Reserved parameter names are escaped everywhere they appear: the - // wrapper signature and the C call. Unreserved names pass through. - assert!( - code.contains("def self.load(end_, in_, key)"), - "escaped params: {code}" - ); - assert!( - code.contains("weaveffi_kw_load(end_, in_, key, err)"), - "escaped call args: {code}" - ); - // Reserved field names are escaped in accessors, keyword arguments, - // assignments, equality, and the value-buffer codecs. - assert!(code.contains("attr_reader :end_"), "accessor: {code}"); - assert!( - code.contains("def initialize(end_:, normal:)"), - "kwargs: {code}" - ); - assert!(code.contains("@end_ = end_"), "assignment: {code}"); - assert!( - code.contains("return false unless end_ == other.end_"), - "equality: {code}" - ); - assert!( - code.contains("w.write_string(v.end_)"), - "codec pack: {code}" - ); - assert!( - code.contains("Config.new(end_: _wv_end_, normal: _wv_normal)"), - "codec unpack: {code}" - ); - // No bare reserved spelling survives in an identifier position. - assert!(!code.contains("attr_reader :end\n"), "raw keyword: {code}"); - assert!(!code.contains("def self.load(end,"), "raw param: {code}"); -} - -#[test] -fn negative_runtime_codes_fall_through_to_generic_error() { - let code = render(&kv_api(), "WeaveFFI", "weaveffi"); - // The code-to-class table maps exactly the declared (positive) codes. - let table = code - .split("KV_ERROR_CODES = {") - .nth(1) - .expect("codes table") - .split("}.freeze") - .next() - .expect("table body"); - assert!(table.contains("1001 => KvError::KeyNotFound,"), "{table}"); - assert!(table.contains("1004 => KvError::IoError,"), "{table}"); - assert!(!table.contains('-'), "no negative codes mapped: {table}"); - // The factory sends any code outside the table, the negative runtime - // range included, to the generic branded Error. - assert!( - code.contains("cls = KV_ERROR_CODES[code]"), - "table lookup: {code}" - ); - assert!( - code.contains("return Error.new(code, message) if cls.nil?"), - "generic fallback: {code}" - ); - // The trap path never consults the domain table: a non-throwing - // function routes its out-err slot through the generic checker, which - // raises the plain branded Error for panics and marshalling failures. - let ping = code - .split("def self.kv_ping()") - .nth(1) - .expect("kv_ping wrapper"); - let body = ping.split("\n end").next().expect("wrapper body"); - assert!(body.contains("check_error!(err)"), "trap path: {code}"); - assert!(!body.contains("check_kv_error!"), "trap path: {code}"); - assert!( - code.contains("raise Error.new(code, msg)"), - "generic checker raises branded Error: {code}" - ); -} - -#[test] -fn gemspec_escapes_quotes_and_backslashes_in_metadata() { - use weaveffi_ir::ir::Package; - - let mut api = make_api(vec![simple_module("kv", vec![])]).api().clone(); - api.package = Some(Package { - name: "my-kv".into(), - version: "1.2.3".into(), - description: Some("A 'quoted' description with \\ backslash".into()), - license: Some("MIT OR 'Custom'".into()), - authors: vec!["O'Brien".into()], - homepage: Some("https://example.com/it's".into()), - repository: None, - }); - let api = ResolvedApi::assume_resolved(api); - let dir = tempfile::tempdir().unwrap(); - let out_dir = Utf8Path::from_path(dir.path()).unwrap(); - RubyGenerator - .generate(&api, out_dir, &RubyConfig::default()) - .unwrap(); - let spec = std::fs::read_to_string(dir.path().join("ruby/my-kv.gemspec")).unwrap(); - assert!( - spec.contains(r"s.summary = 'A \'quoted\' description with \\ backslash'"), - "summary escaped: {spec}" - ); - assert!( - spec.contains(r"s.license = 'MIT OR \'Custom\''"), - "license escaped: {spec}" - ); - assert!( - spec.contains(r"s.authors = ['O\'Brien']"), - "authors escaped: {spec}" - ); - assert!( - spec.contains(r"s.homepage = 'https://example.com/it\'s'"), - "homepage escaped: {spec}" - ); -} diff --git a/crates/weaveffi-gen-ruby/src/types.rs b/crates/weaveffi-gen-ruby/src/types.rs index ce5999d9..ff3c3380 100644 --- a/crates/weaveffi-gen-ruby/src/types.rs +++ b/crates/weaveffi-gen-ruby/src/types.rs @@ -5,7 +5,7 @@ use heck::ToSnakeCase; use weaveffi_core::abi::{AbiParam, CType}; use weaveffi_core::lang::{escape_ident, RUBY_KEYWORDS}; -use weaveffi_ir::ir::TypeRef; +use weaveffi_core::model::Ty; /// The Ruby spelling of a user-chosen parameter name: snake_case via heck, /// then keyword-escaped (a reserved name like `end` gains a trailing `_`). @@ -63,38 +63,38 @@ pub(crate) fn rb_abi_types(params: &[AbiParam], string_as_pointer: bool) -> Vec< /// This is ABI-slot vocabulary, not wire vocabulary: a typed handle crosses /// a `next` slot as an opaque pointer, so anything non-scalar reads a /// pointer. -pub(crate) fn rb_read_method(ty: &TypeRef) -> &'static str { +pub(crate) fn rb_read_method(ty: &Ty) -> &'static str { match ty { - TypeRef::I8 => "read_int8", - TypeRef::I16 => "read_int16", - TypeRef::I32 | TypeRef::Bool | TypeRef::Enum(_) => "read_int32", - TypeRef::U8 => "read_uint8", - TypeRef::U16 => "read_uint16", - TypeRef::U32 => "read_uint32", - TypeRef::I64 => "read_int64", - TypeRef::U64 => "read_uint64", - TypeRef::F32 => "read_float", - TypeRef::F64 => "read_double", - TypeRef::Handle => "read_uint64", + Ty::I8 => "read_int8", + Ty::I16 => "read_int16", + Ty::I32 | Ty::Bool | Ty::Enum(_) => "read_int32", + Ty::U8 => "read_uint8", + Ty::U16 => "read_uint16", + Ty::U32 => "read_uint32", + Ty::I64 => "read_int64", + Ty::U64 => "read_uint64", + Ty::F32 => "read_float", + Ty::F64 => "read_double", + Ty::Handle => "read_uint64", _ => "read_pointer", } } /// The `FFI::MemoryPointer` element type allocated for one iterator element /// out-slot, mirroring [`rb_read_method`]. -pub(crate) fn rb_mem_type(ty: &TypeRef) -> &'static str { +pub(crate) fn rb_mem_type(ty: &Ty) -> &'static str { match ty { - TypeRef::I8 => ":int8", - TypeRef::I16 => ":int16", - TypeRef::I32 | TypeRef::Bool | TypeRef::Enum(_) => ":int32", - TypeRef::U8 => ":uint8", - TypeRef::U16 => ":uint16", - TypeRef::U32 => ":uint32", - TypeRef::I64 => ":int64", - TypeRef::U64 => ":uint64", - TypeRef::F32 => ":float", - TypeRef::F64 => ":double", - TypeRef::Handle => ":uint64", + Ty::I8 => ":int8", + Ty::I16 => ":int16", + Ty::I32 | Ty::Bool | Ty::Enum(_) => ":int32", + Ty::U8 => ":uint8", + Ty::U16 => ":uint16", + Ty::U32 => ":uint32", + Ty::I64 => ":int64", + Ty::U64 => ":uint64", + Ty::F32 => ":float", + Ty::F64 => ":double", + Ty::Handle => ":uint64", _ => ":pointer", } } diff --git a/crates/weaveffi-gen-swift/src/calls.rs b/crates/weaveffi-gen-swift/src/calls.rs index b96aaa3d..38d7c357 100644 --- a/crates/weaveffi-gen-swift/src/calls.rs +++ b/crates/weaveffi-gen-swift/src/calls.rs @@ -12,12 +12,12 @@ use heck::ToLowerCamelCase; use weaveffi_core::codegen::CodeWriter; use weaveffi_core::errors::ERROR_BRAND; use weaveffi_core::lang; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ CallShape, FnBinding, IteratorBinding, ListenerBinding, ModuleBinding, ParamBinding, }; use weaveffi_core::plan::{ret_pass, ArgPass, ElemFree, ErrorStrategy, RetPass}; use weaveffi_core::utils::{local_type_name, wrapper_name}; -use weaveffi_ir::ir::TypeRef; use crate::codec::{fresh, read_value_stmts, write_value_stmts}; use crate::docs::emit_fn_doc; @@ -291,14 +291,14 @@ fn build_c_call_args(params: &[ParamBinding], c_prefix: &str, module_name: &str) } } ArgPass::Direct { .. } => match &p.ty { - TypeRef::Enum(enum_name) => args.push(format!( + Ty::Enum(enum_name) => args.push(format!( "{}({}.rawValue)", c_enum_type(enum_name, c_prefix, module_name), p.name )), // A typed handle is a `UInt64` token in Swift; the C slot is // an opaque typed pointer, so reinterpret the bits. - TypeRef::TypedHandle(_) => { + Ty::TypedHandle(_) => { args.push(format!("OpaquePointer(bitPattern: UInt({}))", p.name)); } _ => args.push(p.name.clone()), @@ -327,8 +327,8 @@ fn raw_return_swift( RetPass::String => "UnsafePointer?".to_string(), RetPass::Object { .. } => "OpaquePointer?".to_string(), RetPass::Direct => match f.ret.as_ref() { - Some(TypeRef::TypedHandle(_)) => "OpaquePointer?".to_string(), - Some(TypeRef::Enum(name)) => c_enum_type(name, c_prefix, module_name), + Some(Ty::TypedHandle(_)) => "OpaquePointer?".to_string(), + Some(Ty::Enum(name)) => c_enum_type(name, c_prefix, module_name), Some(other) => swift_type_for(other), None => unreachable!("a direct return carries a type"), }, @@ -339,9 +339,9 @@ fn raw_return_swift( /// nullable). fn ret_interface_name(f: &FnBinding) -> &str { match f.ret.as_ref() { - Some(TypeRef::Interface(name)) => name, - Some(TypeRef::Optional(inner)) => match inner.as_ref() { - TypeRef::Interface(name) => name, + Some(Ty::Interface(name)) => name, + Some(Ty::Optional(inner)) => match inner.as_ref() { + Ty::Interface(name) => name, _ => unreachable!("non-interface optional is buffered"), }, _ => unreachable!("object return implies an interface type"), @@ -543,11 +543,11 @@ fn render_return_tail( )); } Some(RetPass::Direct) => match f.ret.as_ref() { - Some(TypeRef::Enum(name)) => { + Some(Ty::Enum(name)) => { let ty_name = ctx.ty_name(local_type_name(name)); w.line(format!("return {ty_name}(rawValue: rv.rawValue)!")); } - Some(TypeRef::TypedHandle(_)) => { + Some(Ty::TypedHandle(_)) => { w.line("return UInt64(UInt(bitPattern: rv))"); } _ => { @@ -562,10 +562,10 @@ fn render_return_tail( /// owning Swift class would `*_destroy` a borrowed handle on ARC release. /// Buffered parameters are decoded to their idiomatic value types before the /// closure is invoked. -fn swift_cb_param_type(ty: &TypeRef, ctx: SwiftCtx) -> String { +fn swift_cb_param_type(ty: &Ty, ctx: SwiftCtx) -> String { match ty { - TypeRef::Interface(_) => "OpaquePointer?".into(), - TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Interface(_)) => { + Ty::Interface(_) => "OpaquePointer?".into(), + Ty::Optional(inner) if matches!(inner.as_ref(), Ty::Interface(_)) => { "OpaquePointer?".into() } other => swift_type_ctx(other, ctx), @@ -586,16 +586,16 @@ fn cb_slot_ident(name: &str) -> String { fn swift_cb_direct_arg(p: &ParamBinding, ctx: SwiftCtx) -> String { let n0 = cb_slot_ident(&p.abi[0].name); match &p.ty { - TypeRef::Enum(_) => { + Ty::Enum(_) => { let local = swift_type_ctx(&p.ty, ctx); format!("{local}(rawValue: {n0}.rawValue)!") } - TypeRef::StringUtf8 | TypeRef::BorrowedStr => format!("String(cString: {n0}!)"), - TypeRef::Bytes | TypeRef::BorrowedBytes => { + Ty::StringUtf8 | Ty::BorrowedStr => format!("String(cString: {n0}!)"), + Ty::Bytes | Ty::BorrowedBytes => { let n1 = cb_slot_ident(&p.abi[1].name); format!("{n0}.map {{ Data(bytes: $0, count: {n1}) }} ?? Data()") } - TypeRef::TypedHandle(_) => format!("UInt64(UInt(bitPattern: {n0}))"), + Ty::TypedHandle(_) => format!("UInt64(UInt(bitPattern: {n0}))"), // Interfaces (and nullable interfaces) stay raw borrowed pointers. _ => n0, } @@ -983,13 +983,13 @@ fn render_async_resume_result( )); } RetPass::Direct => match f.ret.as_ref() { - Some(TypeRef::Enum(name)) => { + Some(Ty::Enum(name)) => { let ty_name = ctx.ty_name(local_type_name(name)); w.line(format!( "contRef.value.resume(returning: {ty_name}(rawValue: result.rawValue)!)" )); } - Some(TypeRef::TypedHandle(_)) => { + Some(Ty::TypedHandle(_)) => { w.line("contRef.value.resume(returning: UInt64(UInt(bitPattern: result)))"); } _ => { @@ -1032,7 +1032,7 @@ pub(crate) fn render_swift_iterator_class( let stem = domain_stem(mb); let throws = protocol.error == ErrorStrategy::Throws; let has_len_slot = protocol.elem_free == ElemFree::Bytes; - let is_bytes_elem = matches!(inner, TypeRef::Bytes | TypeRef::BorrowedBytes); + let is_bytes_elem = matches!(inner, Ty::Bytes | Ty::BorrowedBytes); // A `(ptr, len)` element that isn't raw bytes is a buffered value the // wrapper decodes. let is_buffered_elem = has_len_slot && !is_bytes_elem; @@ -1055,13 +1055,13 @@ pub(crate) fn render_swift_iterator_class( // The `out_item` slot declaration. let (c_var, default): (String, String) = match inner { _ if has_len_slot => ("UnsafePointer?".to_string(), "nil".to_string()), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => { + Ty::StringUtf8 | Ty::BorrowedStr => { ("UnsafePointer?".to_string(), "nil".to_string()) } - TypeRef::Interface(_) | TypeRef::TypedHandle(_) | TypeRef::Optional(_) => { + Ty::Interface(_) | Ty::TypedHandle(_) | Ty::Optional(_) => { ("OpaquePointer?".to_string(), "nil".to_string()) } - TypeRef::Enum(_) => (elem_c_type.clone(), format!("{elem_c_type}(0)")), + Ty::Enum(_) => (elem_c_type.clone(), format!("{elem_c_type}(0)")), _ => (swift_type_for(inner), swift_scalar_default(inner)), }; @@ -1174,14 +1174,14 @@ pub(crate) fn render_swift_iterator_class( w.line("return element"); } else { let convert = match inner { - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "String(cString: item!)".to_string(), + Ty::StringUtf8 | Ty::BorrowedStr => "String(cString: item!)".to_string(), // An owned interface element is adopted by the wrapper class, // whose deinit owes the `_destroy`. - TypeRef::Interface(name) => { + Ty::Interface(name) => { format!("{}(ptr: item!)", ctx.ty_name(local_type_name(name))) } - TypeRef::TypedHandle(_) => "UInt64(UInt(bitPattern: item))".to_string(), - TypeRef::Enum(name) => format!( + Ty::TypedHandle(_) => "UInt64(UInt(bitPattern: item))".to_string(), + Ty::Enum(name) => format!( "{}(rawValue: item.rawValue)!", ctx.ty_name(local_type_name(name)) ), diff --git a/crates/weaveffi-gen-swift/src/codec.rs b/crates/weaveffi-gen-swift/src/codec.rs index c0695c53..7c80329a 100644 --- a/crates/weaveffi-gen-swift/src/codec.rs +++ b/crates/weaveffi-gen-swift/src/codec.rs @@ -4,12 +4,12 @@ //! Both emitters dispatch on the shared [`wire::classify`] classification, //! so the non-obvious folds (handles as `u64` tokens, borrowed views like //! their owned forms, records and rich enums through one user codec) are -//! decided centrally rather than re-derived from `TypeRef` here. +//! decided centrally rather than re-derived from `Ty` here. use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; +use weaveffi_core::model::{Prim, WireType}; use weaveffi_core::utils::local_type_name; -use weaveffi_core::wire::{self, WireType}; -use weaveffi_ir::ir::TypeRef; use crate::types::{swift_type_ctx, SwiftCtx}; @@ -26,55 +26,55 @@ pub(crate) fn fresh(counter: &mut usize, prefix: &str) -> String { /// delegating records and rich enums to their generated `wvWrite*` codecs. pub(crate) fn write_value_stmts( w: &mut CodeWriter, - ty: &TypeRef, + ty: &Ty, expr: &str, writer: &str, ctx: SwiftCtx, counter: &mut usize, ) { - match wire::classify(ty) { - WireType::Bool => { + match ty.wire() { + WireType::Prim(Prim::Bool) => { w.line(format!("{writer}.writeBool({expr})")); } - WireType::I8 => { + WireType::Prim(Prim::I8) => { w.line(format!("{writer}.writeInt8({expr})")); } - WireType::U8 => { + WireType::Prim(Prim::U8) => { w.line(format!("{writer}.writeUInt8({expr})")); } - WireType::I16 => { + WireType::Prim(Prim::I16) => { w.line(format!("{writer}.writeInt16({expr})")); } - WireType::U16 => { + WireType::Prim(Prim::U16) => { w.line(format!("{writer}.writeUInt16({expr})")); } - WireType::I32 => { + WireType::Prim(Prim::I32) => { w.line(format!("{writer}.writeInt32({expr})")); } - WireType::U32 => { + WireType::Prim(Prim::U32) => { w.line(format!("{writer}.writeUInt32({expr})")); } - WireType::I64 => { + WireType::Prim(Prim::I64) => { w.line(format!("{writer}.writeInt64({expr})")); } - WireType::U64 => { + WireType::Prim(Prim::U64) => { w.line(format!("{writer}.writeUInt64({expr})")); } - WireType::F32 => { + WireType::Prim(Prim::F32) => { w.line(format!("{writer}.writeFloat({expr})")); } - WireType::F64 => { + WireType::Prim(Prim::F64) => { w.line(format!("{writer}.writeDouble({expr})")); } - WireType::String => { + WireType::Prim(Prim::String) => { w.line(format!("{writer}.writeString({expr})")); } - WireType::Bytes => { + WireType::Prim(Prim::Bytes) => { w.line(format!("{writer}.writeBytes({expr})")); } // Both handle flavors surface as `UInt64` in Swift, so one write // covers them. - WireType::Handle => { + WireType::Handle(_) => { w.line(format!("{writer}.writeUInt64({expr})")); } WireType::Enum(name) => { @@ -134,53 +134,53 @@ pub(crate) fn write_value_stmts( /// `WvReader` variable named `reader`, binding the result to `out`. pub(crate) fn read_value_stmts( w: &mut CodeWriter, - ty: &TypeRef, + ty: &Ty, out: &str, reader: &str, ctx: SwiftCtx, counter: &mut usize, ) { - match wire::classify(ty) { - WireType::Bool => { + match ty.wire() { + WireType::Prim(Prim::Bool) => { w.line(format!("let {out} = {reader}.readBool()")); } - WireType::I8 => { + WireType::Prim(Prim::I8) => { w.line(format!("let {out} = {reader}.readInt8()")); } - WireType::U8 => { + WireType::Prim(Prim::U8) => { w.line(format!("let {out} = {reader}.readUInt8()")); } - WireType::I16 => { + WireType::Prim(Prim::I16) => { w.line(format!("let {out} = {reader}.readInt16()")); } - WireType::U16 => { + WireType::Prim(Prim::U16) => { w.line(format!("let {out} = {reader}.readUInt16()")); } - WireType::I32 => { + WireType::Prim(Prim::I32) => { w.line(format!("let {out} = {reader}.readInt32()")); } - WireType::U32 => { + WireType::Prim(Prim::U32) => { w.line(format!("let {out} = {reader}.readUInt32()")); } - WireType::I64 => { + WireType::Prim(Prim::I64) => { w.line(format!("let {out} = {reader}.readInt64()")); } - WireType::U64 => { + WireType::Prim(Prim::U64) => { w.line(format!("let {out} = {reader}.readUInt64()")); } - WireType::F32 => { + WireType::Prim(Prim::F32) => { w.line(format!("let {out} = {reader}.readFloat()")); } - WireType::F64 => { + WireType::Prim(Prim::F64) => { w.line(format!("let {out} = {reader}.readDouble()")); } - WireType::String => { + WireType::Prim(Prim::String) => { w.line(format!("let {out} = {reader}.readString()")); } - WireType::Bytes => { + WireType::Prim(Prim::Bytes) => { w.line(format!("let {out} = {reader}.readBytes()")); } - WireType::Handle => { + WireType::Handle(_) => { w.line(format!("let {out} = {reader}.readUInt64()")); } WireType::Enum(name) => { diff --git a/crates/weaveffi-gen-swift/src/lib.rs b/crates/weaveffi-gen-swift/src/lib.rs index a6abdf49..8d6d972e 100644 --- a/crates/weaveffi-gen-swift/src/lib.rs +++ b/crates/weaveffi-gen-swift/src/lib.rs @@ -22,8 +22,6 @@ mod docs; mod entities; mod package; mod runtime; -#[cfg(test)] -mod tests; mod types; use std::collections::{HashMap, HashSet}; @@ -245,8 +243,6 @@ impl LanguageBackend for SwiftGenerator { } } -weaveffi_core::impl_generator_via_backend!(SwiftGenerator); - /// Render the complete generated Swift wrapper file: prelude and imports, /// the runtime helpers the model needs, every module's file-scope types, and /// one namespace `enum` per top-level module. diff --git a/crates/weaveffi-gen-swift/src/package.rs b/crates/weaveffi-gen-swift/src/package.rs index 91a4a663..6a3350e0 100644 --- a/crates/weaveffi-gen-swift/src/package.rs +++ b/crates/weaveffi-gen-swift/src/package.rs @@ -8,20 +8,24 @@ use heck::ToUpperCamelCase; use weaveffi_core::package::PackageContext; +use weaveffi_core::resolved::ResolvedApi; use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; -use weaveffi_ir::ir::Api; use crate::types::swift_str; use crate::SwiftConfig; /// The SwiftPM package/module name: an explicit `[swift] module_name` wins; -/// otherwise the IDL `package:` name (PascalCased to a legal Swift module) -/// drives it; falling back to the `WeaveFFI` brand. -pub(crate) fn resolve_module_name(api: &Api, config: &SwiftConfig) -> String { +/// otherwise the `[package] name` from `weaveffi.toml` (PascalCased to a +/// legal Swift module) drives it; falling back to the `WeaveFFI` brand. +pub(crate) fn resolve_module_name(api: &ResolvedApi, config: &SwiftConfig) -> String { config .module_name .clone() - .or_else(|| api.package.as_ref().map(|p| p.name.to_upper_camel_case())) + .or_else(|| { + api.package() + .and_then(|p| p.name.as_deref()) + .map(ToUpperCamelCase::to_upper_camel_case) + }) .unwrap_or_else(|| "WeaveFFI".to_string()) } diff --git a/crates/weaveffi-gen-swift/src/tests.rs b/crates/weaveffi-gen-swift/src/tests.rs deleted file mode 100644 index 8343799e..00000000 --- a/crates/weaveffi-gen-swift/src/tests.rs +++ /dev/null @@ -1,3524 +0,0 @@ -use super::*; -use crate::types::swift_type_for; -use weaveffi_core::codegen::Generator; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_ir::ir::{ - Api, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, Module, Param, StructDef, - StructField, TypeRef, -}; - -fn make_api(modules: Vec) -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: "0.7.0".to_string(), - modules, - generators: None, - package: None, - }) -} - -/// Build the binding model and render the wrapper, exactly as the driver -/// does in production before calling [`LanguageBackend::files`]. -fn render( - api: &ResolvedApi, - c_prefix: &str, - strip_module_prefix: bool, - input_basename: &str, - filename: &str, -) -> String { - let model = BindingModel::build(api, c_prefix); - render_swift_wrapper( - api, - &model, - c_prefix, - strip_module_prefix, - input_basename, - filename, - ) -} - -fn empty_module(name: &str) -> Module { - Module { - name: name.into(), - functions: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - } -} - -#[test] -fn package_uses_binary_target_and_bundles_slices() { - use camino::Utf8Path; - use weaveffi_core::package::{FileContent, PackageContext}; - use weaveffi_core::platform::{BinarySet, Platform}; - - let api = make_api(vec![empty_module("calc")]); - let model = BindingModel::build(&api, "weaveffi"); - let mut bins = BinarySet::new("calculator"); - bins.insert(Platform::MacosArm64, "/s/darwin-arm64/libcalculator.dylib"); - bins.insert(Platform::MacosX64, "/s/darwin-x64/libcalculator.dylib"); - let ctx = PackageContext { - binaries: &bins, - input_basename: Some("calculator.yml"), - }; - let files = LanguageBackend::package( - &SwiftGenerator, - &api, - &model, - &ctx, - Utf8Path::new("/out"), - &SwiftConfig::default(), - ) - .expect("swift supports packaging"); - - assert_eq!(files.iter().filter(|f| f.is_binary()).count(), 2); - assert!(files.iter().any(|f| f - .path - .as_str() - .ends_with("swift/lib/darwin-arm64/libcalculator.dylib"))); - let pkg = files - .iter() - .find(|f| f.path.as_str().ends_with("swift/Package.swift")) - .expect("Package.swift present"); - let FileContent::Text(txt) = &pkg.content else { - panic!("Package.swift is text"); - }; - assert!( - txt.contains(".binaryTarget(") && txt.contains(".xcframework"), - "binaryTarget xcframework missing: {txt}" - ); -} - -#[test] -fn listeners_generate_register_unregister() { - use weaveffi_ir::ir::{CallbackDef, ListenerDef}; - let api = make_api(vec![Module { - name: "events".into(), - functions: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![CallbackDef { - name: "OnMessage".into(), - doc: None, - params: vec![Param { - name: "message".into(), - ty: TypeRef::StringUtf8, - mutable: false, - doc: None, - }], - }], - listeners: vec![ListenerDef { - name: "message_listener".into(), - event_callback: "OnMessage".into(), - doc: None, - }], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let swift = render(&api, "weaveffi", false, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - swift.contains("final class WvCallbackBox"), - "callback box must be emitted: {swift}" - ); - assert!( - swift.contains( - "public static func eventsRegisterMessageListener(_ callback: @escaping (String) -> Void) -> UInt64" - ), - "register wrapper missing: {swift}" - ); - assert!( - swift.contains("public static func eventsUnregisterMessageListener(_ id: UInt64)"), - "unregister wrapper missing: {swift}" - ); - assert!( - swift.contains("cb(String(cString: message!))"), - "trampoline must convert the string arg: {swift}" - ); - assert!( - swift.contains("Unmanaged.passRetained(box).toOpaque()"), - "closure box must be retained through context: {swift}" - ); - assert!( - swift.contains(".fromOpaque(ctx).release()"), - "unregister must release the retained box: {swift}" - ); -} - -#[test] -fn listener_decodes_buffered_argument() { - use weaveffi_ir::ir::{CallbackDef, ListenerDef}; - let api = make_api(vec![Module { - name: "events".into(), - functions: vec![], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![StructField { - name: "name".into(), - ty: TypeRef::StringUtf8, - doc: None, - }], - }], - enums: vec![], - callbacks: vec![CallbackDef { - name: "OnContact".into(), - doc: None, - params: vec![Param { - name: "contact".into(), - ty: TypeRef::Record("Contact".into()), - mutable: false, - doc: None, - }], - }], - listeners: vec![ListenerDef { - name: "contact_listener".into(), - event_callback: "OnContact".into(), - doc: None, - }], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let swift = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - // The user closure receives the decoded value type, not raw pointers. - assert!( - swift.contains("_ callback: @escaping (Contact) -> Void"), - "listener closure must take the decoded record: {swift}" - ); - // The borrowed (ptr, len) pair is copied and decoded inside the - // trampoline, before the user closure is invoked, and never freed. - assert!( - swift.contains( - "let contactBuf = [UInt8](UnsafeBufferPointer(start: contact_ptr, count: contact_len))" - ), - "trampoline must copy the borrowed buffer: {swift}" - ); - assert!( - swift.contains("wvReadContact(&contactReader)"), - "trampoline must decode the record: {swift}" - ); - assert!( - swift.contains("cb(v0)"), - "trampoline must pass the decoded value: {swift}" - ); -} - -#[test] -fn swift_type_for_struct_returns_name() { - assert_eq!( - swift_type_for(&TypeRef::Record("Contact".into())), - "Contact" - ); -} - -#[test] -fn swift_type_for_enum_returns_name() { - assert_eq!(swift_type_for(&TypeRef::Enum("Color".into())), "Color"); -} - -#[test] -fn swift_type_for_optional_wraps_inner() { - assert_eq!( - swift_type_for(&TypeRef::Optional(Box::new(TypeRef::I32))), - "Int32?" - ); - assert_eq!( - swift_type_for(&TypeRef::Optional(Box::new(TypeRef::Record( - "Contact".into() - )))), - "Contact?" - ); -} - -#[test] -fn swift_type_for_list_wraps_inner() { - assert_eq!( - swift_type_for(&TypeRef::List(Box::new(TypeRef::I32))), - "[Int32]" - ); - assert_eq!( - swift_type_for(&TypeRef::List(Box::new(TypeRef::Enum("Color".into())))), - "[Color]" - ); -} - -fn plain_fn(name: &str, params: Vec, returns: Option) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - } -} - -fn p(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - mutable: false, - doc: None, - } -} - -fn field(name: &str, ty: TypeRef) -> StructField { - StructField { - name: name.into(), - ty, - doc: None, - } -} - -fn variant(name: &str, value: i32) -> EnumVariant { - EnumVariant { - name: name.into(), - value, - doc: None, - fields: vec![], - } -} - -#[test] -fn render_enum_declaration() { - let api = make_api(vec![Module { - name: "paint".to_string(), - functions: vec![], - structs: vec![], - enums: vec![EnumDef { - name: "Color".to_string(), - doc: None, - variants: vec![variant("Red", 0), variant("Green", 1), variant("Blue", 2)], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("public enum Color: UInt32 {"), - "missing enum declaration: {out}" - ); - assert!(out.contains("case red = 0"), "missing red variant: {out}"); - assert!( - out.contains("case green = 1"), - "missing green variant: {out}" - ); - assert!(out.contains("case blue = 2"), "missing blue variant: {out}"); -} - -#[test] -fn render_enum_variant_camel_case() { - let api = make_api(vec![Module { - name: "status".to_string(), - functions: vec![], - structs: vec![], - enums: vec![EnumDef { - name: "Status".to_string(), - doc: None, - variants: vec![variant("InProgress", 0), variant("AllDone", 1)], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("case inProgress = 0"), - "missing camelCase variant: {out}" - ); - assert!( - out.contains("case allDone = 1"), - "missing camelCase variant: {out}" - ); -} - -#[test] -fn render_function_with_enum_param_and_return() { - let api = make_api(vec![Module { - name: "paint".to_string(), - functions: vec![plain_fn( - "mix", - vec![p("a", TypeRef::Enum("Color".into()))], - Some(TypeRef::Enum("Color".into())), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!(out.contains("a: Color"), "missing enum param type: {out}"); - assert!( - out.contains("-> Color {"), - "missing enum return type: {out}" - ); - assert!( - out.contains("weaveffi_paint_Color(a.rawValue)"), - "missing enum-to-C conversion: {out}" - ); - assert!( - out.contains("Color(rawValue: rv.rawValue)!"), - "missing C-to-enum conversion: {out}" - ); -} - -#[test] -fn render_function_with_optional_value_param() { - let api = make_api(vec![Module { - name: "store".to_string(), - functions: vec![plain_fn( - "find", - vec![p("id", TypeRef::Optional(Box::new(TypeRef::I32)))], - None, - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("id: Int32?"), - "missing optional param type: {out}" - ); - // The optional is packed into a value buffer: flag byte plus payload. - assert!( - out.contains("var idWriter = WvWriter()"), - "missing buffer writer staging: {out}" - ); - assert!( - out.contains("idWriter.writeOptionFlag(true)") - && out.contains("idWriter.writeOptionFlag(false)"), - "missing option flag encoding: {out}" - ); - assert!( - out.contains("idWriter.writeInt32(v0)"), - "missing payload encoding: {out}" - ); - assert!( - out.contains("idWriter.bytes.withUnsafeBufferPointer { id_buf in"), - "missing buffer staging closure: {out}" - ); - assert!( - out.contains("weaveffi_store_find(id_ptr, id_len, &err)"), - "missing two-slot buffered call: {out}" - ); -} - -#[test] -fn render_function_with_optional_struct_param() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![plain_fn( - "update", - vec![p( - "person", - TypeRef::Optional(Box::new(TypeRef::Record("Contact".into()))), - )], - None, - )], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("person: Contact?"), - "missing optional struct param: {out}" - ); - assert!( - out.contains("wvWriteContact(v0, into: &personWriter)"), - "optional record must pack through the record codec: {out}" - ); - assert!( - out.contains("weaveffi_contacts_update(person_ptr, person_len, &err)"), - "missing buffered call: {out}" - ); -} - -#[test] -fn render_function_with_optional_value_return() { - let api = make_api(vec![Module { - name: "store".to_string(), - functions: vec![plain_fn( - "lookup", - vec![p("key", TypeRef::I32)], - Some(TypeRef::Optional(Box::new(TypeRef::I32))), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("-> Int32? {"), - "missing optional return type: {out}" - ); - assert!( - out.contains("var outLen: Int = 0"), - "missing outLen declaration: {out}" - ); - assert!( - out.contains("rvReader.readOptionFlag()"), - "missing option flag decode: {out}" - ); - assert!( - out.contains("weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen)"), - "returned buffer must be freed after copying: {out}" - ); -} - -#[test] -fn render_function_with_optional_string_return() { - let api = make_api(vec![Module { - name: "store".to_string(), - functions: vec![plain_fn( - "get_name", - vec![], - Some(TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("-> String? {"), - "missing optional string return type: {out}" - ); - // An optional string is buffered: flag byte plus length-prefixed UTF-8. - assert!( - out.contains("rvReader.readOptionFlag()") && out.contains("rvReader.readString()"), - "missing buffered optional-string decode: {out}" - ); - assert!( - out.contains("weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen)"), - "returned buffer must be freed after decoding: {out}" - ); -} - -#[test] -fn render_function_with_list_param() { - let api = make_api(vec![Module { - name: "batch".to_string(), - functions: vec![plain_fn( - "process", - vec![p("ids", TypeRef::List(Box::new(TypeRef::I32)))], - None, - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("ids: [Int32]"), - "missing list param type: {out}" - ); - assert!( - out.contains("idsWriter.writeLen(ids.count)"), - "missing count prefix: {out}" - ); - assert!( - out.contains("for v0 in ids {") && out.contains("idsWriter.writeInt32(v0)"), - "missing element encoding loop: {out}" - ); - assert!( - out.contains("idsWriter.bytes.withUnsafeBufferPointer { ids_buf in"), - "missing withUnsafeBufferPointer: {out}" - ); - assert!( - out.contains("weaveffi_batch_process(ids_ptr, ids_len, &err)"), - "missing buffered call: {out}" - ); -} - -#[test] -fn render_function_with_list_return() { - let api = make_api(vec![Module { - name: "batch".to_string(), - functions: vec![plain_fn( - "get_ids", - vec![], - Some(TypeRef::List(Box::new(TypeRef::I32))), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("-> [Int32] {"), - "missing list return type: {out}" - ); - assert!( - out.contains("var outLen: Int = 0"), - "missing outLen declaration: {out}" - ); - assert!(out.contains("&outLen"), "missing outLen in call: {out}"); - assert!( - out.contains("rvReader.readLen()"), - "missing count decode: {out}" - ); - assert!( - out.contains("v0.append(v2)") && out.contains("return v0"), - "missing element decode loop: {out}" - ); -} - -#[test] -fn render_function_with_optional_struct_return() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![plain_fn( - "find", - vec![p("id", TypeRef::I32)], - Some(TypeRef::Optional(Box::new(TypeRef::Record( - "Contact".into(), - )))), - )], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("-> Contact? {"), - "missing optional struct return: {out}" - ); - assert!( - out.contains("wvReadContact(&rvReader)"), - "optional record must decode through the record codec: {out}" - ); -} - -#[test] -fn render_buffer_runtime_is_always_emitted() { - let api = make_api(vec![]); - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("struct WvWriter"), - "missing buffer writer: {out}" - ); - assert!( - out.contains("struct WvReader"), - "missing buffer reader: {out}" - ); - assert!( - out.contains("func wvDecodeFailure"), - "missing decode-failure trap: {out}" - ); - // Little-endian assembly, one byte at a time (no alignment assumptions). - assert!( - out.contains("v |= UInt32(b) << (8 * i)"), - "reader must assemble little-endian: {out}" - ); - assert!( - out.contains("bytes.append(UInt8(truncatingIfNeeded: v >> 8))"), - "writer must emit little-endian: {out}" - ); - // Malformed input rejection: truncation, invalid flags, oversized - // length prefixes, and trailing bytes. - assert!( - out.contains("length prefix exceeds remaining buffer"), - "missing length-prefix validation: {out}" - ); - assert!( - out.contains("trailing bytes after value"), - "missing trailing-bytes validation: {out}" - ); - assert!( - out.contains("option flag byte out of range"), - "missing flag validation: {out}" - ); -} - -#[test] -fn render_record_as_swift_struct() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![], - structs: vec![StructDef { - name: "Contact".to_string(), - doc: None, - fields: vec![ - field("name", TypeRef::StringUtf8), - field("age", TypeRef::I32), - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("public struct Contact {"), - "record must be a plain struct: {out}" - ); - assert!( - out.contains("public var name: String") && out.contains("public var age: Int32"), - "missing stored properties: {out}" - ); - assert!( - out.contains("public init(name: String, age: Int32) {"), - "missing public memberwise init: {out}" - ); - // One pack and one unpack routine, fields in declaration order. - assert!( - out.contains("func wvWriteContact(_ value: Contact, into w: inout WvWriter) {"), - "missing pack routine: {out}" - ); - assert!( - out.contains("w.writeString(value.name)") && out.contains("w.writeInt32(value.age)"), - "pack must write fields in order: {out}" - ); - assert!( - out.contains("func wvReadContact(_ r: inout WvReader) -> Contact {"), - "missing unpack routine: {out}" - ); - assert!( - out.contains("return Contact(name: v0, age: v1)"), - "unpack must rebuild the struct: {out}" - ); - // No class wrapping, no C symbols, no getters, no builders. - assert!( - !out.contains("public class Contact"), - "record must not be a class: {out}" - ); - assert!( - !out.contains("Contact_destroy") && !out.contains("deinit"), - "record has no destroy: {out}" - ); - assert!( - !out.contains("Contact_get_name"), - "record has no FFI getters: {out}" - ); - assert!( - !out.contains("ContactBuilder"), - "record has no builder: {out}" - ); -} - -#[test] -fn swift_custom_prefix_threads_to_user_symbols() { - let api = make_api(vec![Module { - name: "demo".to_string(), - functions: vec![plain_fn( - "paint", - vec![p("c", TypeRef::Enum("Color".into()))], - Some(TypeRef::Enum("Color".into())), - )], - structs: vec![StructDef { - name: "Point".to_string(), - doc: None, - fields: vec![field("x", TypeRef::I32), field("y", TypeRef::I32)], - }], - enums: vec![EnumDef { - name: "Color".to_string(), - doc: None, - variants: vec![variant("Red", 0), variant("Green", 1)], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let tmp = std::env::temp_dir().join("weaveffi_test_swift_custom_prefix"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8"); - let config = SwiftConfig { - prefix: Some("myffi".to_string()), - ..Default::default() - }; - SwiftGenerator.generate(&api, out_dir, &config).unwrap(); - - let swift = std::fs::read_to_string( - tmp.join("swift") - .join("Sources") - .join("WeaveFFI") - .join("WeaveFFI.swift"), - ) - .unwrap(); - let modulemap = std::fs::read_to_string( - tmp.join("swift") - .join("Sources") - .join("CWeaveFFI") - .join("module.modulemap"), - ) - .unwrap(); - let _ = std::fs::remove_dir_all(&tmp); - - // User symbols honor the configured ABI prefix: the function symbol and - // the enum-to-C cast both carry `myffi_`. - assert!( - swift.contains("myffi_demo_paint"), - "function user symbol should use custom prefix: {swift}" - ); - assert!( - swift.contains("myffi_demo_Color("), - "enum-cast user symbol should use custom prefix: {swift}" - ); - // No user symbol falls back to the hard-coded `weaveffi_` prefix. - assert!( - !swift.contains("weaveffi_demo_"), - "no user symbol should keep the default prefix: {swift}" - ); - // The system module map points at the prefixed C header. - assert!( - modulemap.contains("header \"../../../c/myffi.h\""), - "module map should reference the prefixed C header: {modulemap}" - ); - // Runtime ABI helpers stay literal regardless of the prefix. - assert!( - swift.contains("weaveffi_error_clear(&err)"), - "runtime helper must remain literal: {swift}" - ); -} - -#[test] -fn render_function_returning_struct() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![plain_fn( - "create", - vec![p("age", TypeRef::I32)], - Some(TypeRef::Record("Contact".into())), - )], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("age", TypeRef::I32)], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("-> Contact {"), - "missing struct return type: {out}" - ); - // A record return is a buffered return: ptr plus trailing out_len. - assert!( - out.contains("let rv = weaveffi_contacts_create(age, &outLen, &err)"), - "missing buffered call: {out}" - ); - assert!( - out.contains("wvReadContact(&rvReader)"), - "missing record decode: {out}" - ); - assert!( - out.contains("weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen)"), - "returned buffer must be freed after copying: {out}" - ); - assert!( - out.contains("rvReader.finish()"), - "decode must reject trailing bytes: {out}" - ); -} - -#[test] -fn render_function_with_struct_param() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![plain_fn( - "save", - vec![p("contact", TypeRef::Record("Contact".into()))], - None, - )], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("contact: Contact"), - "missing struct param type: {out}" - ); - // The record is packed into a caller-owned buffer and passed as two - // slots; the callee never frees it. - assert!( - out.contains("var contactWriter = WvWriter()") - && out.contains("wvWriteContact(contact, into: &contactWriter)"), - "missing record packing: {out}" - ); - assert!( - out.contains("weaveffi_contacts_save(contact_ptr, contact_len, &err)"), - "missing two-slot buffered call: {out}" - ); - assert!( - !out.contains("contact.ptr"), - "record param must not marshal as an object pointer: {out}" - ); -} - -#[test] -fn render_struct_with_bytes_field() { - let api = make_api(vec![Module { - name: "storage".to_string(), - functions: vec![], - structs: vec![StructDef { - name: "Blob".to_string(), - doc: None, - fields: vec![field("data", TypeRef::Bytes)], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("public var data: Data"), - "missing bytes property: {out}" - ); - assert!( - out.contains("w.writeBytes(value.data)"), - "pack must length-prefix the bytes: {out}" - ); - assert!( - out.contains("let v0 = r.readBytes()"), - "unpack must decode the bytes: {out}" - ); -} - -#[test] -fn render_struct_with_nested_struct_field() { - let api = make_api(vec![Module { - name: "geo".to_string(), - functions: vec![], - structs: vec![ - StructDef { - name: "Point".to_string(), - doc: None, - fields: vec![field("x", TypeRef::I32)], - }, - StructDef { - name: "Line".to_string(), - doc: None, - fields: vec![field("start", TypeRef::Record("Point".into()))], - }, - ], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("public var start: Point"), - "missing nested struct property: {out}" - ); - // Nested records delegate to the inner codec. - assert!( - out.contains("wvWritePoint(value.start, into: &w)"), - "pack must delegate to the nested codec: {out}" - ); - assert!( - out.contains("let v0 = wvReadPoint(&r)"), - "unpack must delegate to the nested codec: {out}" - ); -} - -#[test] -fn render_function_returning_struct_with_buffer_params() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![plain_fn( - "find_by_name", - vec![p("query", TypeRef::StringUtf8)], - Some(TypeRef::Record("Contact".into())), - )], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("-> Contact {"), - "missing struct return type with buffer params: {out}" - ); - // The staged closures return the raw buffer pointer; the decode happens - // after the closures complete. - assert!( - out.contains("let rv: UnsafePointer? = query.withCString { query_ptr in"), - "missing annotated closure binding: {out}" - ); - assert!( - out.contains("return weaveffi_contacts_find_by_name(query_ptr, &outLen, &err)"), - "missing buffered call inside the closure: {out}" - ); - assert!( - out.contains("wvReadContact(&rvReader)"), - "missing record decode after the call: {out}" - ); -} - -#[test] -fn generate_swift_with_structs_and_enums() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![plain_fn( - "get_contact", - vec![p("id", TypeRef::I32)], - Some(TypeRef::Record("Contact".into())), - )], - structs: vec![StructDef { - name: "Contact".to_string(), - doc: None, - fields: vec![ - field("name", TypeRef::StringUtf8), - field("email", TypeRef::StringUtf8), - field("age", TypeRef::I32), - ], - }], - enums: vec![EnumDef { - name: "Color".to_string(), - doc: None, - variants: vec![variant("Red", 0), variant("Green", 1), variant("Blue", 2)], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let tmp = std::env::temp_dir().join("weaveffi_test_swift_structs_and_enums"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8"); - - SwiftGenerator - .generate( - &api, - out_dir, - &SwiftConfig { - strip_module_prefix: true, - ..SwiftConfig::default() - }, - ) - .unwrap(); - - let swift = std::fs::read_to_string( - tmp.join("swift") - .join("Sources") - .join("WeaveFFI") - .join("WeaveFFI.swift"), - ) - .unwrap(); - - assert!( - swift.contains("public enum Color: UInt32 {"), - "missing enum declaration: {swift}" - ); - assert!(swift.contains("case red = 0"), "missing red case: {swift}"); - assert!( - swift.contains("case green = 1"), - "missing green case: {swift}" - ); - assert!( - swift.contains("case blue = 2"), - "missing blue case: {swift}" - ); - - assert!( - swift.contains("public struct Contact {"), - "missing struct declaration: {swift}" - ); - assert!( - swift.contains("public var name: String") - && swift.contains("public var email: String") - && swift.contains("public var age: Int32"), - "missing stored properties: {swift}" - ); - - assert!( - swift.contains("public static func getContact(id: Int32) -> Contact {"), - "missing function signature: {swift}" - ); - assert!( - swift.contains("wvReadContact(&rvReader)"), - "missing record decode: {swift}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn swift_type_for_map() { - assert_eq!( - swift_type_for(&TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32) - )), - "[String: Int32]" - ); - assert_eq!( - swift_type_for(&TypeRef::Map( - Box::new(TypeRef::I32), - Box::new(TypeRef::F64) - )), - "[Int32: Double]" - ); -} - -#[test] -fn render_function_with_map_param() { - let api = make_api(vec![Module { - name: "store".to_string(), - functions: vec![plain_fn( - "update_scores", - vec![p( - "scores", - TypeRef::Map(Box::new(TypeRef::I32), Box::new(TypeRef::F64)), - )], - None, - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("scores: [Int32: Double]"), - "missing map param type: {out}" - ); - // The map packs into one buffer: count then alternating key, value. - assert!( - out.contains("scoresWriter.writeLen(scores.count)"), - "missing count prefix: {out}" - ); - assert!( - out.contains("for (v0, v1) in scores {"), - "missing entry loop: {out}" - ); - assert!( - out.contains("scoresWriter.writeInt32(v0)") && out.contains("scoresWriter.writeDouble(v1)"), - "missing key/value encoding: {out}" - ); - assert!( - out.contains("weaveffi_store_update_scores(scores_ptr, scores_len, &err)"), - "missing single-buffer call: {out}" - ); -} - -#[test] -fn render_function_with_map_return() { - let api = make_api(vec![Module { - name: "store".to_string(), - functions: vec![plain_fn( - "get_scores", - vec![], - Some(TypeRef::Map(Box::new(TypeRef::I32), Box::new(TypeRef::F64))), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("-> [Int32: Double] {"), - "missing map return type: {out}" - ); - assert!(out.contains("var outLen: Int = 0"), "missing outLen: {out}"); - assert!( - out.contains("var v0: [Int32: Double] = [:]"), - "missing dict construction: {out}" - ); - assert!( - out.contains("v0[v2] = v3"), - "missing entry decode loop: {out}" - ); - // No parallel key/value arrays remain. - assert!( - !out.contains("outKeysPtr") && !out.contains("outValuesPtr"), - "map return must not use parallel arrays: {out}" - ); -} - -#[test] -fn swift_struct_optional_fields_are_properties() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![], - structs: vec![StructDef { - name: "Contact".to_string(), - doc: None, - fields: vec![ - field("email", TypeRef::Optional(Box::new(TypeRef::StringUtf8))), - field("age", TypeRef::Optional(Box::new(TypeRef::I32))), - field( - "role", - TypeRef::Optional(Box::new(TypeRef::Enum("Role".into()))), - ), - ], - }], - enums: vec![EnumDef { - name: "Role".into(), - doc: None, - variants: vec![variant("Admin", 0), variant("User", 1)], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - - assert!( - out.contains("public var email: String?"), - "missing optional string property: {out}" - ); - assert!( - out.contains("public var age: Int32?"), - "missing optional i32 property: {out}" - ); - assert!( - out.contains("public var role: Role?"), - "missing optional enum property: {out}" - ); - // The codec writes the option flag per field. - assert!( - out.contains("w.writeOptionFlag(true)") && out.contains("w.writeOptionFlag(false)"), - "codec must encode option flags: {out}" - ); - assert!( - out.contains("Role(rawValue: UInt32(bitPattern: r.readInt32()))!"), - "codec must decode the enum from its i32 wire form: {out}" - ); -} - -#[test] -fn swift_custom_module_name() { - let api = make_api(vec![Module { - name: "math".to_string(), - functions: vec![plain_fn( - "add", - vec![p("a", TypeRef::I32), p("b", TypeRef::I32)], - Some(TypeRef::I32), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let config = SwiftConfig { - module_name: Some("MyCoolLib".into()), - ..SwiftConfig::default() - }; - - let tmp = std::env::temp_dir().join("weaveffi_test_swift_custom_module"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8"); - - SwiftGenerator.generate(&api, out_dir, &config).unwrap(); - - let pkg = std::fs::read_to_string(tmp.join("swift").join("Package.swift")).unwrap(); - assert!( - pkg.contains("name: \"MyCoolLib\""), - "Package.swift should use custom module name: {pkg}" - ); - assert!( - pkg.contains("\"CMyCoolLib\""), - "Package.swift should reference CMyCoolLib: {pkg}" - ); - assert!( - !pkg.contains("\"WeaveFFI\""), - "Package.swift should not reference WeaveFFI as a module name: {pkg}" - ); - - let modulemap = std::fs::read_to_string( - tmp.join("swift") - .join("Sources") - .join("CMyCoolLib") - .join("module.modulemap"), - ) - .unwrap(); - assert!( - modulemap.contains("module CMyCoolLib"), - "modulemap should use custom name: {modulemap}" - ); - - let swift_src = tmp - .join("swift") - .join("Sources") - .join("MyCoolLib") - .join("MyCoolLib.swift"); - assert!( - swift_src.exists(), - "Swift source should be at MyCoolLib/MyCoolLib.swift" - ); - - let swift = std::fs::read_to_string(&swift_src).unwrap(); - assert!( - swift.contains("import CMyCoolLib"), - "wrapper must import the renamed C module: {swift}" - ); - assert!( - !swift.contains("import CWeaveFFI"), - "wrapper must not import the default C module when renamed: {swift}" - ); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn swift_inline_error_types() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![plain_fn( - "get", - vec![p("id", TypeRef::I32)], - Some(TypeRef::I32), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: Some(ErrorDomain { - name: "ContactError".to_string(), - codes: vec![ - ErrorCode { - name: "ContactNotFound".to_string(), - code: 1001, - message: "Contact not found".to_string(), - doc: None, - fields: vec![], - }, - ErrorCode { - name: "InvalidInput".to_string(), - code: 1002, - message: "Invalid input provided".to_string(), - doc: None, - fields: vec![], - }, - ], - }), - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - - // The generic brand error keeps only the unknown-code case; the - // domain gets its own typed enum. - assert!( - out.contains("public enum WeaveFFIError: Error, LocalizedError {"), - "missing brand error: {out}" - ); - assert!( - out.contains("public enum ContactError: Error, LocalizedError {"), - "missing typed error enum: {out}" - ); - assert!( - out.contains("case contactNotFound(message: String)"), - "missing contactNotFound case: {out}" - ); - assert!( - out.contains("case invalidInput(message: String)"), - "missing invalidInput case: {out}" - ); - assert!( - out.contains("public var errorDescription: String?"), - "missing errorDescription property: {out}" - ); - assert!( - out.contains("public var errorCode: Int32"), - "missing errorCode property: {out}" - ); - assert!( - out.contains("case .contactNotFound: return 1001"), - "missing contactNotFound code: {out}" - ); - assert!( - out.contains("case .invalidInput: return 1002"), - "missing invalidInput code: {out}" - ); - assert!( - out.contains( - "func mapContacts(code: Int32, message: String, payload: [UInt8]?) -> Error {" - ), - "missing domain mapper: {out}" - ); - assert!( - out.contains( - "case 1001: return ContactError.contactNotFound(message: message.isEmpty ? \"Contact not found\" : message)" - ), - "missing contactNotFound mapping: {out}" - ); - assert!( - out.contains( - "case 1002: return ContactError.invalidInput(message: message.isEmpty ? \"Invalid input provided\" : message)" - ), - "missing invalidInput mapping: {out}" - ); - assert!( - out.contains("default: return WeaveFFIError.error(code: code, message: message)"), - "missing unknown-code fallback: {out}" - ); - assert!( - out.contains("func checkContacts(_ err: inout weaveffi_error) throws {"), - "missing domain checker: {out}" - ); - assert!( - out.contains("throw mapContacts(code: code, message: message, payload: payload)"), - "domain checker must throw through the mapper: {out}" - ); -} - -#[test] -fn swift_error_payload_fields_decode() { - let api = make_api(vec![Module { - name: "kv".to_string(), - functions: vec![Function { - name: "put".into(), - params: vec![p("key", TypeRef::StringUtf8)], - returns: None, - doc: None, - throws: true, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: Some(ErrorDomain { - name: "KvError".to_string(), - codes: vec![ - ErrorCode { - name: "KeyNotFound".to_string(), - code: 1001, - message: "Key not found".to_string(), - doc: None, - fields: vec![], - }, - ErrorCode { - name: "QuotaExceeded".to_string(), - code: 1002, - message: "Quota exceeded".to_string(), - doc: None, - fields: vec![field("limit", TypeRef::U32), field("used", TypeRef::U32)], - }, - ], - }), - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - - // A code with declared fields carries them as labeled associated values - // after the message. - assert!( - out.contains("case quotaExceeded(message: String, limit: UInt32, used: UInt32)"), - "missing payload associated values: {out}" - ); - assert!( - out.contains("case keyNotFound(message: String)"), - "field-less code keeps the message-only shape: {out}" - ); - // errorDescription wildcards the payload fields. - assert!( - out.contains("case let .quotaExceeded(message, _, _): return message"), - "errorDescription must wildcard payload fields: {out}" - ); - // The mapper decodes the payload buffer in declaration order. - assert!( - out.contains("var payloadReader = WvReader(bytes: payload ?? [])"), - "mapper must read the payload buffer: {out}" - ); - assert!( - out.contains("payloadReader.readUInt32()"), - "mapper must decode the fields: {out}" - ); - assert!( - out.contains("payloadReader.finish()"), - "mapper must reject trailing payload bytes: {out}" - ); - assert!( - out.contains("limit: v0, used: v1"), - "mapper must label the decoded fields: {out}" - ); - // The checker copies the payload before clearing the error slot. - assert!( - out.contains( - "let payload: [UInt8]? = err.payload_ptr.map { [UInt8](UnsafeBufferPointer(start: $0, count: err.payload_len)) }" - ), - "checker must copy the payload before clearing: {out}" - ); - // The error slot initializer covers the payload fields. - assert!( - out.contains("weaveffi_error(code: 0, message: nil, payload_ptr: nil, payload_len: 0)"), - "error slot init must cover the payload fields: {out}" - ); -} - -#[test] -fn swift_struct_list_field_is_property() { - let api = make_api(vec![Module { - name: "store".to_string(), - functions: vec![], - structs: vec![StructDef { - name: "Order".to_string(), - doc: None, - fields: vec![ - field("item_ids", TypeRef::List(Box::new(TypeRef::I32))), - field("tags", TypeRef::List(Box::new(TypeRef::Enum("Tag".into())))), - ], - }], - enums: vec![EnumDef { - name: "Tag".into(), - doc: None, - variants: vec![variant("New", 0), variant("Sale", 1)], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - - // Field names camel-case into Swift property names. - assert!( - out.contains("public var itemIds: [Int32]"), - "missing list i32 property: {out}" - ); - assert!( - out.contains("public var tags: [Tag]"), - "missing list enum property: {out}" - ); - assert!( - out.contains("w.writeLen(value.itemIds.count)"), - "codec must count-prefix the list: {out}" - ); - assert!( - out.contains("Tag(rawValue: UInt32(bitPattern: r.readInt32()))!"), - "codec must decode the enum elements: {out}" - ); -} - -#[test] -fn swift_strip_module_prefix() { - let api = make_api(vec![Module { - name: "contacts".to_string(), - functions: vec![plain_fn( - "create_contact", - vec![p("name", TypeRef::StringUtf8)], - Some(TypeRef::I32), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let config = SwiftConfig { - strip_module_prefix: true, - ..SwiftConfig::default() - }; - - let tmp = std::env::temp_dir().join("weaveffi_test_swift_strip_prefix"); - let _ = std::fs::remove_dir_all(&tmp); - std::fs::create_dir_all(&tmp).unwrap(); - let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8"); - - SwiftGenerator.generate(&api, out_dir, &config).unwrap(); - - let swift = std::fs::read_to_string(tmp.join("swift/Sources/WeaveFFI/WeaveFFI.swift")).unwrap(); - - assert!( - swift.contains("func createContact("), - "stripped name should be createContact: {swift}" - ); - assert!( - !swift.contains("func contactsCreateContact("), - "should not contain module-prefixed name: {swift}" - ); - assert!( - swift.contains("weaveffi_contacts_create_contact"), - "C ABI call should still use full name: {swift}" - ); - - let no_strip_config = SwiftConfig { - strip_module_prefix: false, - ..SwiftConfig::default() - }; - let tmp2 = std::env::temp_dir().join("weaveffi_test_swift_no_strip_prefix"); - let _ = std::fs::remove_dir_all(&tmp2); - std::fs::create_dir_all(&tmp2).unwrap(); - let out_dir2 = Utf8Path::from_path(&tmp2).expect("valid UTF-8"); - - SwiftGenerator - .generate(&api, out_dir2, &no_strip_config) - .unwrap(); - - let swift2 = - std::fs::read_to_string(tmp2.join("swift/Sources/WeaveFFI/WeaveFFI.swift")).unwrap(); - - assert!( - swift2.contains("func contactsCreateContact("), - "strip_module_prefix: false should restore the prefixed name: {swift2}" - ); - assert!( - swift2.contains("weaveffi_contacts_create_contact"), - "C ABI call should still use full name: {swift2}" - ); - - let _ = std::fs::remove_dir_all(&tmp); - let _ = std::fs::remove_dir_all(&tmp2); -} - -#[test] -fn swift_deeply_nested_optional() { - let api = make_api(vec![Module { - name: "edge".into(), - functions: vec![plain_fn( - "process", - vec![p( - "data", - TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional( - Box::new(TypeRef::Record("Contact".into())), - ))))), - )], - None, - )], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let swift = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - swift.contains("[Contact?]?"), - "should contain deeply nested optional type: {swift}" - ); -} - -#[test] -fn swift_map_of_lists() { - let api = make_api(vec![Module { - name: "edge".into(), - functions: vec![plain_fn( - "process", - vec![p( - "scores", - TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::List(Box::new(TypeRef::I32))), - ), - )], - None, - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let swift = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - swift.contains("[String: [Int32]]"), - "should contain map of lists type: {swift}" - ); -} - -#[test] -fn swift_enum_keyed_map() { - let api = make_api(vec![Module { - name: "edge".into(), - functions: vec![plain_fn( - "process", - vec![p( - "contacts", - TypeRef::Map( - Box::new(TypeRef::Enum("Color".into())), - Box::new(TypeRef::Record("Contact".into())), - ), - )], - None, - )], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - enums: vec![EnumDef { - name: "Color".into(), - doc: None, - variants: vec![variant("Red", 0), variant("Green", 1), variant("Blue", 2)], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let swift = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - swift.contains("[Color: Contact]"), - "should contain enum-keyed map type: {swift}" - ); -} - -#[test] -fn swift_type_for_borrowed_str() { - assert_eq!(swift_type_for(&TypeRef::BorrowedStr), "String"); -} - -#[test] -fn swift_type_for_borrowed_bytes() { - assert_eq!(swift_type_for(&TypeRef::BorrowedBytes), "Data"); -} - -#[test] -fn swift_function_with_borrowed_str_param() { - let api = make_api(vec![Module { - name: "io".to_string(), - functions: vec![plain_fn( - "write", - vec![p("msg", TypeRef::BorrowedStr)], - None, - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("msg: String"), - "BorrowedStr param should use String type: {out}" - ); - assert!( - out.contains("weaveffi_io_write"), - "should call the C function: {out}" - ); -} - -#[test] -fn swift_function_with_borrowed_bytes_param() { - let api = make_api(vec![Module { - name: "io".to_string(), - functions: vec![plain_fn( - "upload", - vec![p("data", TypeRef::BorrowedBytes)], - None, - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("data: Data"), - "BorrowedBytes param should use Data type: {out}" - ); - assert!( - out.contains("weaveffi_io_upload"), - "should call the C function: {out}" - ); -} - -#[test] -fn swift_typed_handle_is_uint64() { - let api = make_api(vec![Module { - name: "auth".into(), - functions: vec![ - plain_fn( - "revoke", - vec![p("session", TypeRef::TypedHandle("Session".into()))], - None, - ), - plain_fn("open", vec![], Some(TypeRef::TypedHandle("Session".into()))), - ], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let swift = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - // A typed handle is a `UInt64` token in Swift, reinterpreted as the - // opaque typed pointer at the C boundary. - assert!( - swift.contains("session: UInt64"), - "typed handle param must be UInt64: {swift}" - ); - assert!( - swift.contains("OpaquePointer(bitPattern: UInt(session))"), - "typed handle param must convert to the C pointer: {swift}" - ); - assert!( - swift.contains("-> UInt64 {"), - "typed handle return must be UInt64: {swift}" - ); - assert!( - swift.contains("return UInt64(UInt(bitPattern: rv))"), - "typed handle return must convert from the C pointer: {swift}" - ); -} - -#[test] -fn swift_no_double_free_on_error() { - let api = make_api(vec![Module { - name: "contacts".into(), - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - enums: vec![], - callbacks: vec![], - functions: vec![plain_fn( - "find_contact", - vec![p("name", TypeRef::StringUtf8)], - Some(TypeRef::Record("Contact".into())), - )], - interfaces: vec![], - listeners: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - - assert!( - !out.contains("weaveffi_free_string(name"), - "borrowed string param must not be freed by the wrapper: {out}" - ); - - // The error must be checked before the returned buffer is touched. - let fn_start = out - .find("public static func findContact") - .expect("findContact wrapper"); - let fn_body = &out[fn_start..]; - let check_pos = fn_body - .find("trap(&err)") - .expect("trap in findContact (non-throwing wrapper)"); - let decode_pos = fn_body - .find("wvReadContact(") - .expect("record decode in findContact"); - let free_pos = fn_body - .find("weaveffi_free_bytes(") - .expect("buffer free in findContact"); - assert!( - check_pos < decode_pos && check_pos < free_pos, - "error must be checked before decoding or freeing the return: {out}" - ); -} - -#[test] -fn swift_optional_interface_return_maps_null() { - use weaveffi_ir::ir::InterfaceDef; - let api = make_api(vec![Module { - name: "kv".into(), - functions: vec![plain_fn( - "find_store", - vec![p("id", TypeRef::I32)], - Some(TypeRef::Optional(Box::new(TypeRef::Interface( - "Store".into(), - )))), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![InterfaceDef { - name: "Store".into(), - doc: None, - constructors: vec![], - methods: vec![], - statics: vec![], - }], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - // Optional stays a nullable pointer: null means none. - assert!( - out.contains("-> Store? {"), - "missing optional interface return type: {out}" - ); - assert!( - out.contains("return rv.map { Store(ptr: $0) }"), - "optional interface return should map null before wrapping: {out}" - ); -} - -#[test] -fn swift_async_function_signature() { - let api = make_api(vec![Module { - name: "tasks".to_string(), - functions: vec![Function { - name: "run".to_string(), - params: vec![p("id", TypeRef::I32)], - returns: Some(TypeRef::I32), - doc: None, - throws: true, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: Some(ErrorDomain { - name: "TaskError".to_string(), - codes: vec![ErrorCode { - name: "Busy".to_string(), - code: 1, - message: "Busy".to_string(), - doc: None, - fields: vec![], - }], - }), - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("public static func run(id: Int32) async throws -> Int32"), - "missing complete async throws signature: {out}" - ); - assert!( - out.contains("withCheckedThrowingContinuation"), - "throwing async must use the throwing continuation: {out}" - ); - assert!( - out.contains("resume(throwing: mapTasks(code: code, message: msg, payload: payload))"), - "callback must resume with the typed domain error: {out}" - ); -} - -#[test] -fn swift_async_uses_continuation() { - let api = make_api(vec![Module { - name: "tasks".to_string(), - functions: vec![Function { - name: "run".to_string(), - params: vec![p("id", TypeRef::I32)], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - // A non-throwing async function uses the plain continuation flavor - // and traps if the slot ever reports (producer panic). - assert!( - out.contains("public static func run(id: Int32) async -> Int32"), - "missing plain async signature: {out}" - ); - assert!( - out.contains("withCheckedContinuation"), - "missing withCheckedContinuation: {out}" - ); - assert!( - out.contains("CheckedContinuation"), - "plain async must use the Never-typed continuation: {out}" - ); - assert!( - out.contains("ContinuationRef"), - "missing ContinuationRef usage: {out}" - ); - assert!( - out.contains("Unmanaged"), - "missing Unmanaged for context bridging: {out}" - ); - assert!( - out.contains("weaveffi_tasks_run_async"), - "missing async C function call: {out}" - ); -} - -/// `Unmanaged.passRetained(...)` (the +1 retain that pins the -/// continuation across the C boundary) must be matched by exactly one -/// `Unmanaged.fromOpaque(...).takeRetainedValue()` in the C callback so -/// the continuation is released when the future resolves. -#[test] -fn swift_async_pins_callback_for_lifetime() { - let api = make_api(vec![Module { - name: "tasks".to_string(), - functions: vec![Function { - name: "run".to_string(), - params: vec![p("id", TypeRef::I32)], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - let pin_count = out.matches("Unmanaged.passRetained").count(); - let unpin_count = out.matches("takeRetainedValue()").count(); - assert_eq!( - pin_count, 1, - "expected exactly one Unmanaged.passRetained, found {pin_count}: {out}" - ); - assert_eq!( - unpin_count, 1, - "expected exactly one takeRetainedValue, found {unpin_count}: {out}" - ); -} - -#[test] -fn swift_async_buffered_result_decoded_in_callback() { - let api = make_api(vec![Module { - name: "tasks".to_string(), - functions: vec![Function { - name: "fetch_ids".to_string(), - params: vec![], - returns: Some(TypeRef::List(Box::new(TypeRef::I32))), - doc: None, - throws: true, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - // The callback receives an owned (ptr, len) pair. - assert!( - out.contains("{ context, err, resultPtr, resultLen in"), - "callback must take the owned buffer pair: {out}" - ); - // The buffer is copied and decoded inside the callback, before resuming. - assert!( - out.contains( - "let resultBytes = [UInt8](UnsafeBufferPointer(start: resultPtr, count: resultLen))" - ), - "callback must copy the owned buffer: {out}" - ); - assert!( - out.contains("var resultReader = WvReader(bytes: resultBytes)"), - "callback must decode the buffer: {out}" - ); - assert!( - out.contains("contRef.value.resume(returning: v0)"), - "callback must resume with the decoded value: {out}" - ); - // The wrapper owns the buffer: it frees the producer allocation after - // copying. - let cb_pos = out.find("fetch_ids_async").expect("async launcher present"); - assert!( - out[cb_pos..] - .contains("weaveffi_free_bytes(UnsafeMutablePointer(mutating: resultPtr), resultLen)"), - "async wrapper must free the owned result buffer after copying: {out}" - ); -} - -#[test] -fn swift_cross_module_struct() { - let api = make_api(vec![ - Module { - name: "types".to_string(), - functions: vec![], - structs: vec![StructDef { - name: "Name".to_string(), - doc: None, - fields: vec![field("value", TypeRef::StringUtf8)], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }, - Module { - name: "ops".to_string(), - functions: vec![plain_fn( - "get_name", - vec![p("id", TypeRef::I32)], - Some(TypeRef::Record("types.Name".to_string())), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }, - ]); - - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - - assert!( - out.contains("-> Name"), - "cross-module return type should use local name 'Name': {out}" - ); - assert!( - out.contains("wvReadName(&rvReader)"), - "cross-module struct decode should use the local codec name: {out}" - ); - assert!( - !out.contains("types.Name"), - "dot-qualified name should not appear in generated Swift code: {out}" - ); -} - -#[test] -fn swift_nested_module_output() { - let api = make_api(vec![Module { - name: "parent".to_string(), - functions: vec![plain_fn("outer_fn", vec![], Some(TypeRef::I32))], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![Module { - name: "child".to_string(), - functions: vec![plain_fn("inner_fn", vec![], Some(TypeRef::I32))], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }], - }]); - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("public enum Parent {"), - "top-level module enum missing: {out}" - ); - assert!( - out.contains("public enum Child {"), - "nested module enum missing: {out}" - ); - assert!( - out.contains("weaveffi_parent_outer_fn"), - "parent C ABI call missing: {out}" - ); - assert!( - out.contains("weaveffi_parent_child_inner_fn"), - "nested child C ABI call missing: {out}" - ); -} - -/// A module with an `iter` function, throwing or not. -fn iter_api(throws: bool) -> ResolvedApi { - make_api(vec![Module { - name: "data".to_string(), - functions: vec![Function { - name: "list_items".to_string(), - params: vec![], - returns: Some(TypeRef::Iterator(Box::new(TypeRef::I32))), - doc: None, - throws, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: if throws { - Some(ErrorDomain { - name: "DataError".to_string(), - codes: vec![ErrorCode { - name: "Broken".to_string(), - code: 7, - message: "Broken".to_string(), - doc: None, - fields: vec![], - }], - }) - } else { - None - }, - modules: vec![], - }]) -} - -#[test] -fn swift_iterator_emits_lazy_sequence_class() { - let out = render( - &iter_api(false), - "weaveffi", - true, - "weaveffi.yml", - "WeaveFFI.swift", - ); - // A final class conforming to Sequence & IteratorProtocol owns the - // handle; the wrapper returns it instead of a drained array. - assert!( - out.contains("public final class DataListItemsIterator: Sequence, IteratorProtocol {"), - "missing lazy sequence class: {out}" - ); - assert!( - out.contains("public static func listItems() -> DataListItemsIterator {"), - "wrapper must return the sequence type, not an array: {out}" - ); - assert!( - out.contains("return DataListItemsIterator(handle: rv)"), - "wrapper must hand the launched handle to the sequence: {out}" - ); - // No hidden drain: the wrapper body never loops over `_next`. - assert!( - !out.contains("while weaveffi_data_ListItemsIterator_next"), - "wrapper must not drain the iterator eagerly: {out}" - ); - assert!( - !out.contains("-> [Int32]"), - "iterator wrapper must not return an array: {out}" - ); -} - -#[test] -fn swift_iterator_next_pulls_one_element_and_destroys() { - let out = render( - &iter_api(false), - "weaveffi", - true, - "weaveffi.yml", - "WeaveFFI.swift", - ); - // Exactly one producer `next` call, inside `next()`. - assert_eq!( - out.matches("weaveffi_data_ListItemsIterator_next(").count(), - 1, - "expected exactly one next call (inside next()): {out}" - ); - assert!( - out.contains("public func next() -> Int32? {"), - "missing IteratorProtocol next(): {out}" - ); - // Destroy happens eagerly on exhaustion and from deinit, guarded - // against double-destroy by nulling the handle. - assert!( - out.contains("private func destroyHandle() {"), - "missing destroy helper: {out}" - ); - assert!( - out.contains("guard let handle = handle else { return }") - && out.contains("weaveffi_data_ListItemsIterator_destroy(handle)") - && out.contains("self.handle = nil"), - "destroy must null the handle to prevent double-destroy: {out}" - ); - let deinit_pos = out.find("deinit {").expect("deinit present"); - assert!( - out[deinit_pos..].contains("destroyHandle()"), - "deinit must destroy an abandoned iterator: {out}" - ); - // Non-throwing: a mid-stream error is a producer bug and traps. - assert!( - out.contains("fatalError(\"\\(code): \\(message)\")"), - "non-throwing iterator must trap on per-next errors: {out}" - ); - assert!( - !out.contains("public private(set) var error"), - "non-throwing iterator has no error property: {out}" - ); -} - -#[test] -fn swift_throwing_iterator_stores_per_next_error() { - let out = render( - &iter_api(true), - "weaveffi", - true, - "weaveffi.yml", - "WeaveFFI.swift", - ); - // Launch errors throw through the domain checker in the wrapper. - assert!( - out.contains("public static func listItems() throws -> DataListItemsIterator {"), - "throwing wrapper must keep its throws signature: {out}" - ); - let wrapper_pos = out - .find("public static func listItems()") - .expect("wrapper present"); - assert!( - out[wrapper_pos..].contains("try checkData(&err)"), - "launch errors must throw through the domain checker: {out}" - ); - // Per-next domain errors end iteration and are stored on the sequence. - assert!( - out.contains("public private(set) var error: Error?"), - "throwing iterator must expose the stored error: {out}" - ); - assert!( - out.contains("self.error = mapData(code: code, message: message, payload: payload)"), - "per-next errors must map through the domain: {out}" - ); - let class_pos = out - .find("public final class DataListItemsIterator") - .expect("class present"); - let class_body = &out[class_pos..]; - assert!( - !class_body.contains("fatalError"), - "throwing iterator must not trap on domain errors: {class_body}" - ); -} - -#[test] -fn swift_string_iterator_frees_each_element() { - let api = make_api(vec![Module { - name: "data".to_string(), - functions: vec![plain_fn( - "list_names", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("let element = String(cString: item!)"), - "string element must be copied: {out}" - ); - assert!( - out.contains("weaveffi_free_string(item)"), - "string element must be freed after copying: {out}" - ); -} - -#[test] -fn swift_record_iterator_decodes_and_frees_elements() { - let api = make_api(vec![Module { - name: "kv".to_string(), - functions: vec![plain_fn( - "scan", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::Record("Entry".into())))), - )], - structs: vec![StructDef { - name: "Entry".into(), - doc: None, - fields: vec![field("key", TypeRef::StringUtf8)], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("public func next() -> Entry? {"), - "record iterator must yield the value type: {out}" - ); - // A record element is a buffered element: two out slots, decode then - // free with weaveffi_free_bytes. - assert!( - out.contains("var item: UnsafePointer? = nil") - && out.contains("var itemLen: Int = 0"), - "buffered element must use the (ptr, len) slots: {out}" - ); - assert!( - out.contains("wvReadEntry(&itemReader)"), - "record element must decode through its codec: {out}" - ); - assert!( - out.contains("weaveffi_free_bytes(UnsafeMutablePointer(mutating: item), itemLen)"), - "buffered element must be freed after copying: {out}" - ); -} - -#[test] -fn list_return_frees_buffer_and_decodes_string_elements() { - let api = make_api(vec![Module { - name: "data".to_string(), - functions: vec![ - plain_fn( - "get_ids", - vec![], - Some(TypeRef::List(Box::new(TypeRef::I32))), - ), - plain_fn( - "get_names", - vec![], - Some(TypeRef::List(Box::new(TypeRef::StringUtf8))), - ), - ], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - // The whole encoding is one producer-allocated buffer, released once. - assert!( - out.contains("weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen)"), - "list return must free the value buffer: {out}" - ); - // String elements decode from the buffer; there is no per-element free. - assert!( - out.contains("rvReader.readString()"), - "string list elements must decode from the buffer: {out}" - ); - assert!( - !out.contains("weaveffi_free_string(rv"), - "no per-element string free remains: {out}" - ); -} - -#[test] -fn map_return_decodes_single_buffer() { - let api = make_api(vec![Module { - name: "store".to_string(), - functions: vec![plain_fn( - "get_scores", - vec![], - Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("weaveffi_free_bytes(UnsafeMutablePointer(mutating: rv), outLen)"), - "map return must free the value buffer once: {out}" - ); - assert!( - out.contains("rvReader.readString()") && out.contains("rvReader.readInt32()"), - "map entries must decode alternating key, value: {out}" - ); - assert!( - !out.contains("outKeysPtr") && !out.contains("weaveffi_free_string("), - "no parallel arrays or per-key frees remain: {out}" - ); -} - -#[test] -fn async_string_result_is_copied_then_freed() { - let api = make_api(vec![Module { - name: "tasks".to_string(), - functions: vec![Function { - name: "fetch".to_string(), - params: vec![], - returns: Some(TypeRef::StringUtf8), - doc: None, - throws: true, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - // The callback's string is owned by the consumer: it is copied into the - // resumed value and then released with `weaveffi_free_string`. - assert!( - out.contains("let resultValue = String(cString: result)") - && out.contains("contRef.value.resume(returning: resultValue)"), - "async string result must be copied before resuming: {out}" - ); - let cb_pos = out.find("fetch_async").expect("async launcher present"); - assert!( - out[cb_pos..].contains("weaveffi_free_string(result)"), - "async wrapper must free the owned result string after copying: {out}" - ); -} - -#[test] -fn deprecated_function_generates_annotation() { - let api = make_api(vec![Module { - name: "math".to_string(), - functions: vec![Function { - name: "add_old".to_string(), - params: vec![p("a", TypeRef::I32), p("b", TypeRef::I32)], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: false, - cancellable: false, - deprecated: Some("Use addV2 instead".to_string()), - since: Some("0.1.0".to_string()), - }], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - assert!( - out.contains("@available(*, deprecated, message: \"Use addV2 instead\")"), - "missing deprecation annotation: {out}" - ); - assert!( - out.contains("func addOld("), - "missing function declaration: {out}" - ); -} - -fn doc_api() -> ResolvedApi { - make_api(vec![Module { - name: "docs".into(), - functions: vec![Function { - name: "do_thing".into(), - params: vec![Param { - name: "x".into(), - ty: TypeRef::I32, - mutable: false, - doc: Some("the input value".into()), - }], - returns: Some(TypeRef::I32), - doc: Some("Performs a thing.".into()), - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Item".into(), - doc: Some("An item we track.".into()), - fields: vec![StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: Some("Stable id".into()), - }], - }], - enums: vec![EnumDef { - name: "Kind".into(), - doc: Some("Kind of item.".into()), - variants: vec![EnumVariant { - name: "Small".into(), - value: 0, - doc: Some("A small one".into()), - fields: vec![], - }], - }], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: Some(ErrorDomain { - name: "DocsErrors".into(), - codes: vec![ErrorCode { - name: "not_found".into(), - code: 1, - message: "Not found".into(), - doc: Some("Raised when missing".into()), - fields: vec![], - }], - }), - modules: vec![], - }]) -} - -#[test] -fn swift_emits_doc_on_function() { - let out = render( - &doc_api(), - "weaveffi", - true, - "weaveffi.yml", - "WeaveFFI.swift", - ); - assert!(out.contains("/// Performs a thing."), "{out}"); -} - -#[test] -fn swift_emits_doc_on_struct() { - let out = render( - &doc_api(), - "weaveffi", - true, - "weaveffi.yml", - "WeaveFFI.swift", - ); - assert!(out.contains("/// An item we track."), "{out}"); -} - -#[test] -fn swift_emits_doc_on_enum_variant() { - let out = render( - &doc_api(), - "weaveffi", - true, - "weaveffi.yml", - "WeaveFFI.swift", - ); - assert!(out.contains("/// Kind of item."), "{out}"); - assert!(out.contains("/// A small one"), "{out}"); -} - -#[test] -fn swift_emits_doc_on_field() { - let out = render( - &doc_api(), - "weaveffi", - true, - "weaveffi.yml", - "WeaveFFI.swift", - ); - assert!(out.contains("/// Stable id"), "{out}"); -} - -#[test] -fn swift_emits_doc_on_param() { - let out = render( - &doc_api(), - "weaveffi", - true, - "weaveffi.yml", - "WeaveFFI.swift", - ); - assert!(out.contains("/// - Parameter x: the input value"), "{out}"); -} - -/// The `shapes` sample: a rich (algebraic) enum `Shape` (a unit variant, an -/// f64 payload, two f32 payloads, and a string+u8 payload), a plain C-style -/// enum `Channel`, and the free functions that take/return `Shape` (lowered -/// to `TypeRef::RichEnum`) plus the numerics smoke `sum_bytes`. -fn shapes_api() -> ResolvedApi { - make_api(vec![Module { - name: "shapes".into(), - functions: vec![ - plain_fn( - "describe", - vec![p("shape", TypeRef::RichEnum("Shape".into()))], - Some(TypeRef::StringUtf8), - ), - plain_fn( - "scale", - vec![ - p("shape", TypeRef::RichEnum("Shape".into())), - p("factor", TypeRef::F64), - ], - Some(TypeRef::RichEnum("Shape".into())), - ), - plain_fn( - "sum_bytes", - vec![p("values", TypeRef::List(Box::new(TypeRef::U8)))], - Some(TypeRef::U64), - ), - ], - structs: vec![], - enums: vec![ - EnumDef { - name: "Shape".into(), - doc: Some("An algebraic shape".into()), - variants: vec![ - EnumVariant { - name: "Empty".into(), - value: 0, - doc: Some("The empty shape".into()), - fields: vec![], - }, - EnumVariant { - name: "Circle".into(), - value: 1, - doc: None, - fields: vec![field("radius", TypeRef::F64)], - }, - EnumVariant { - name: "Rectangle".into(), - value: 2, - doc: None, - fields: vec![field("width", TypeRef::F32), field("height", TypeRef::F32)], - }, - EnumVariant { - name: "Labeled".into(), - value: 3, - doc: None, - fields: vec![ - field("label", TypeRef::StringUtf8), - field("count", TypeRef::U8), - ], - }, - ], - }, - EnumDef { - name: "Channel".into(), - doc: None, - variants: vec![variant("Red", 0), variant("Green", 1)], - }, - ], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]) -} - -#[test] -fn rich_enum_emits_native_swift_enum() { - let out = render( - &shapes_api(), - "weaveffi", - false, - "shapes.yml", - "Shapes.swift", - ); - // A native Swift enum with associated values, one case per variant, - // labeled by the field names. - assert!( - out.contains("public enum Shape {"), - "missing native enum: {out}" - ); - assert!(out.contains("case empty"), "missing unit case: {out}"); - assert!( - out.contains("case circle(radius: Double)"), - "missing single-payload case: {out}" - ); - assert!( - out.contains("case rectangle(width: Float, height: Float)"), - "missing two-payload case: {out}" - ); - assert!( - out.contains("case labeled(label: String, count: UInt8)"), - "missing mixed-payload case: {out}" - ); - // No opaque wrapper class, no C symbols, no tag reader, no factories. - assert!( - !out.contains("public class Shape"), - "rich enum must not be a class: {out}" - ); - assert!( - !out.contains("Shape_destroy") && !out.contains("Shape_tag"), - "rich enum has no C symbols: {out}" - ); - // The sibling plain C-style enum is still a raw-value Swift enum. - assert!( - out.contains("public enum Channel: UInt32 {"), - "plain enum regressed: {out}" - ); -} - -#[test] -fn rich_enum_emits_codec_pair() { - let out = render( - &shapes_api(), - "weaveffi", - false, - "shapes.yml", - "Shapes.swift", - ); - assert!( - out.contains("func wvWriteShape(_ value: Shape, into w: inout WvWriter) {"), - "missing pack routine: {out}" - ); - // The writer emits the i32 tag then the variant's fields in order. - assert!( - out.contains("case .empty:") && out.contains("w.writeInt32(0)"), - "unit case must write only its tag: {out}" - ); - assert!( - out.contains("case let .circle(v0):") - && out.contains("w.writeInt32(1)") - && out.contains("w.writeDouble(v0)"), - "payload case must write tag then fields: {out}" - ); - assert!( - out.contains("func wvReadShape(_ r: inout WvReader) -> Shape {"), - "missing unpack routine: {out}" - ); - assert!( - out.contains("return .circle(radius:"), - "unpack must rebuild the case with labels: {out}" - ); - assert!( - out.contains("wvDecodeFailure(\"unknown Shape tag"), - "unpack must reject unknown tags: {out}" - ); -} - -#[test] -fn rich_enum_functions_marshal_buffers() { - let out = render( - &shapes_api(), - "weaveffi", - false, - "shapes.yml", - "Shapes.swift", - ); - // describe(Shape) -> String: packs the enum, frees the returned string. - assert!( - out.contains("public static func shapesDescribe(shape: Shape) -> String {"), - "missing describe signature: {out}" - ); - assert!( - out.contains("wvWriteShape(shape, into: &shapeWriter)"), - "describe must pack the enum: {out}" - ); - assert!( - out.contains("weaveffi_shapes_describe(shape_ptr, shape_len, &err)"), - "describe must pass the buffer pair: {out}" - ); - // scale(Shape, f64) -> Shape: buffer in, buffer out. - assert!( - out.contains("public static func shapesScale(shape: Shape, factor: Double) -> Shape {"), - "missing scale signature: {out}" - ); - assert!( - out.contains("weaveffi_shapes_scale(shape_ptr, shape_len, factor, &outLen, &err)"), - "scale must pass buffer pair plus scalar: {out}" - ); - assert!( - out.contains("wvReadShape(&rvReader)"), - "scale must decode the returned buffer: {out}" - ); - // sum_bytes([u8]) -> u64: numerics smoke. - assert!( - out.contains("public static func shapesSumBytes(values: [UInt8]) -> UInt64 {"), - "missing sum_bytes signature: {out}" - ); -} - -/// A `kv` module with a declared error domain and a `Store` interface -/// exercising every member kind: a plain constructor named `new`, a -/// throwing factory constructor, throwing and non-throwing methods, an -/// async throwing method, and a static. -fn store_api() -> ResolvedApi { - use weaveffi_ir::ir::InterfaceDef; - fn f( - name: &str, - params: Vec, - returns: Option, - throws: bool, - is_async: bool, - ) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - throws, - r#async: is_async, - cancellable: false, - deprecated: None, - since: None, - } - } - make_api(vec![Module { - name: "kv".into(), - functions: vec![f( - "inspect", - vec![p("store", TypeRef::Interface("Store".into()))], - Some(TypeRef::I64), - false, - false, - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![InterfaceDef { - name: "Store".into(), - doc: Some("A key-value store.".into()), - constructors: vec![ - f("new", vec![p("capacity", TypeRef::I64)], None, false, false), - f( - "open", - vec![p("path", TypeRef::StringUtf8)], - None, - true, - false, - ), - ], - methods: vec![ - f( - "put", - vec![ - p("key", TypeRef::StringUtf8), - p("value", TypeRef::StringUtf8), - ], - None, - true, - false, - ), - f("count", vec![], Some(TypeRef::I64), false, false), - f("compact", vec![], Some(TypeRef::I64), true, true), - ], - statics: vec![f( - "default_capacity", - vec![], - Some(TypeRef::I64), - false, - false, - )], - }], - errors: Some(ErrorDomain { - name: "KvError".into(), - codes: vec![ErrorCode { - name: "KeyNotFound".into(), - code: 1001, - message: "Key not found".into(), - doc: None, - fields: vec![], - }], - }), - modules: vec![], - }]) -} - -#[test] -fn interface_emits_final_class_with_deinit() { - let out = render(&store_api(), "weaveffi", true, "kv.yml", "Kv.swift"); - assert!( - out.contains("public final class Store {"), - "missing final class: {out}" - ); - assert!( - out.contains("let ptr: OpaquePointer"), - "missing handle property: {out}" - ); - assert!( - out.contains("init(ptr: OpaquePointer) {"), - "missing ownership-adopting init: {out}" - ); - assert!( - out.contains("deinit {\n weaveffi_kv_Store_destroy(ptr)"), - "deinit must call the destroy symbol: {out}" - ); -} - -#[test] -fn interface_ctor_new_becomes_init() { - let out = render(&store_api(), "weaveffi", true, "kv.yml", "Kv.swift"); - // Non-throwing `new` with a labeled parameter. - assert!( - out.contains("public init(capacity: Int64) {"), - "missing public init: {out}" - ); - assert!( - out.contains("let rv = weaveffi_kv_Store_new(capacity, &err)"), - "init must call the constructor symbol: {out}" - ); - assert!( - out.contains("self.ptr = rv"), - "init must adopt the returned handle: {out}" - ); -} - -#[test] -fn interface_secondary_ctor_is_throwing_factory() { - let out = render(&store_api(), "weaveffi", true, "kv.yml", "Kv.swift"); - assert!( - out.contains("public static func open(path: String) throws -> Store {"), - "missing factory signature: {out}" - ); - assert!( - out.contains("let rv: OpaquePointer? = path.withCString { path_ptr in"), - "factory must stage the string param: {out}" - ); - assert!( - out.contains("return weaveffi_kv_Store_open(path_ptr, &err)"), - "factory must call the constructor symbol: {out}" - ); - assert!( - out.contains("try checkKv(&err)"), - "throwing factory must use the domain checker: {out}" - ); - assert!( - out.contains("return Store(ptr: rv)"), - "factory must wrap the owned pointer: {out}" - ); -} - -#[test] -fn interface_methods_pass_self_pointer() { - let out = render(&store_api(), "weaveffi", true, "kv.yml", "Kv.swift"); - // Throwing instance method with labeled string params: `ptr` leads - // the C argument list. - assert!( - out.contains("public func put(key: String, value: String) throws -> Void {"), - "missing throwing method: {out}" - ); - assert!( - out.contains("weaveffi_kv_Store_put(ptr, key_ptr, value_ptr, &err)"), - "method must pass ptr as the leading C argument: {out}" - ); - // Non-throwing instance method traps instead. - assert!( - out.contains("public func count() -> Int64 {"), - "missing plain method: {out}" - ); - let count_body = &out[out.find("public func count()").expect("count body")..]; - assert!( - count_body.contains("weaveffi_kv_Store_count(ptr, &err)") - && count_body.contains("trap(&err)"), - "plain method must call with ptr and trap: {out}" - ); -} - -#[test] -fn interface_async_method_is_async_throws() { - let out = render(&store_api(), "weaveffi", true, "kv.yml", "Kv.swift"); - assert!( - out.contains("public func compact() async throws -> Int64 {"), - "missing async throws method: {out}" - ); - assert!( - out.contains("weaveffi_kv_Store_compact_async(ptr, {"), - "async launcher must lead with ptr: {out}" - ); - assert!( - out.contains("resume(throwing: mapKv(code: code, message: msg, payload: payload))"), - "async method must resume with the typed domain error: {out}" - ); -} - -#[test] -fn interface_static_is_static_func() { - let out = render(&store_api(), "weaveffi", true, "kv.yml", "Kv.swift"); - assert!( - out.contains("public static func defaultCapacity() -> Int64 {"), - "missing static func: {out}" - ); - assert!( - out.contains("weaveffi_kv_Store_default_capacity(&err)"), - "static must call its member symbol: {out}" - ); -} - -#[test] -fn interface_param_passes_borrowed_pointer() { - let out = render(&store_api(), "weaveffi", true, "kv.yml", "Kv.swift"); - // Free function taking the interface: the class is the Swift type and - // the call borrows its handle. - assert!( - out.contains("public static func inspect(store: Store) -> Int64 {"), - "missing interface-typed param signature: {out}" - ); - assert!( - out.contains("weaveffi_kv_inspect(store.ptr, &err)"), - "interface param must pass .ptr: {out}" - ); -} - -#[test] -fn throws_split_on_free_functions() { - let api = make_api(vec![Module { - name: "calc".into(), - functions: vec![ - Function { - name: "div".into(), - params: vec![p("a", TypeRef::I32), p("b", TypeRef::I32)], - returns: Some(TypeRef::I32), - doc: None, - throws: true, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }, - plain_fn( - "add", - vec![p("a", TypeRef::I32), p("b", TypeRef::I32)], - Some(TypeRef::I32), - ), - ], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: Some(ErrorDomain { - name: "CalcError".into(), - codes: vec![ErrorCode { - name: "DivisionByZero".into(), - code: 1, - message: "Division by zero".into(), - doc: None, - fields: vec![], - }], - }), - modules: vec![], - }]); - let out = render(&api, "weaveffi", true, "calc.yml", "Calc.swift"); - // throws: true -> `throws` signature checked through the domain. - assert!( - out.contains("public static func div(a: Int32, b: Int32) throws -> Int32 {"), - "missing throwing signature: {out}" - ); - let div_body = &out[out.find("func div(").expect("div body")..]; - assert!( - div_body.contains("try checkCalc(&err)"), - "throwing fn must use the domain checker: {out}" - ); - // throws: false -> plain signature; the slot still traps. - assert!( - out.contains("public static func add(a: Int32, b: Int32) -> Int32 {"), - "missing plain signature: {out}" - ); - let add_body = &out[out.find("func add(").expect("add body")..]; - assert!( - add_body.contains("trap(&err)"), - "plain fn must trap on a reported error: {out}" - ); - // The trapping helper is the fatalError path. - assert!( - out.contains("fatalError(\"\\(code): \\(message)\")"), - "trap helper must fatalError: {out}" - ); -} - -#[test] -fn strip_module_prefix_defaults_to_true() { - assert!( - SwiftConfig::default().strip_module_prefix, - "stripping must be the default" - ); - // The default config produces stripped, camel-cased names end to end. - let api = make_api(vec![Module { - name: "contacts".into(), - functions: vec![plain_fn( - "create_contact", - vec![p("display_name", TypeRef::StringUtf8)], - Some(TypeRef::I32), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let model = BindingModel::build(&api, "weaveffi"); - let files = LanguageBackend::files( - &SwiftGenerator, - &api, - &model, - Utf8Path::new("/out"), - &SwiftConfig::default(), - ); - let wrapper = files - .iter() - .find(|f| f.path.as_str().ends_with("WeaveFFI.swift")) - .expect("wrapper file"); - assert!( - wrapper - .contents - .contains("public static func createContact(displayName: String) -> Int32 {"), - "default must strip the module prefix and camel the parameter: {}", - wrapper.contents - ); - assert!( - !wrapper.contents.contains("contactsCreateContact"), - "default must not emit prefixed names: {}", - wrapper.contents - ); -} - -#[test] -fn keyword_params_are_escaped() { - let api = make_api(vec![Module { - name: "kw".into(), - functions: vec![plain_fn( - "load", - vec![ - p("import", TypeRef::StringUtf8), - p("class", TypeRef::I32), - p("normal", TypeRef::Bool), - ], - Some(TypeRef::I32), - )], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - // Reserved parameter names gain the shared trailing underscore in the - // signature, and every derived local follows the escaped spelling. - assert!( - out.contains( - "public static func load(import_: String, class_: Int32, normal: Bool) -> Int32 {" - ), - "reserved parameter names must be escaped: {out}" - ); - assert!( - out.contains("import_.withCString { import__ptr in"), - "staged locals must derive from the escaped name: {out}" - ); - assert!( - out.contains("weaveffi_kw_load(import__ptr, class_, normal, &err)"), - "the C call must pass the escaped bindings: {out}" - ); -} - -#[test] -fn keyword_fields_are_escaped() { - let api = make_api(vec![Module { - name: "kw".into(), - functions: vec![plain_fn( - "configure", - vec![p("options", TypeRef::Record("Options".into()))], - Some(TypeRef::Record("Options".into())), - )], - structs: vec![StructDef { - name: "Options".into(), - doc: None, - fields: vec![ - field("default", TypeRef::Bool), - field("in", TypeRef::StringUtf8), - field("plain", TypeRef::I32), - ], - }], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: None, - modules: vec![], - }]); - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - // Reserved field names are escaped in the property, the initializer, and - // both codec directions. - assert!( - out.contains("public var default_: Bool") && out.contains("public var in_: String"), - "reserved field names must be escaped in properties: {out}" - ); - assert!( - out.contains("public init(default_: Bool, in_: String, plain: Int32) {"), - "the memberwise init must use the escaped labels: {out}" - ); - assert!( - out.contains("w.writeBool(value.default_)") && out.contains("w.writeString(value.in_)"), - "the pack codec must read the escaped properties: {out}" - ); - assert!( - out.contains("return Options(default_: v0, in_: v1, plain: v2)"), - "the unpack codec must label the escaped fields: {out}" - ); - // Non-reserved names pass through untouched. - assert!( - out.contains("public var plain: Int32"), - "plain names must not gain an underscore: {out}" - ); -} - -#[test] -fn package_swift_escapes_quotes_in_module_name() { - let api = make_api(vec![empty_module("calc")]); - let model = BindingModel::build(&api, "weaveffi"); - let files = LanguageBackend::files( - &SwiftGenerator, - &api, - &model, - Utf8Path::new("/out"), - &SwiftConfig { - module_name: Some("Weird\"Name\\".into()), - ..SwiftConfig::default() - }, - ); - let pkg = files - .iter() - .find(|f| f.path.as_str().ends_with("Package.swift")) - .expect("Package.swift present"); - assert!( - pkg.contents.contains("name: \"Weird\\\"Name\\\\\""), - "quotes and backslashes in the module name must be escaped: {}", - pkg.contents - ); - assert!( - !pkg.contents.contains("name: \"Weird\"Name"), - "an unescaped quote would corrupt the manifest: {}", - pkg.contents - ); -} - -/// Domain error codes are validated positive; the reserved negative runtime -/// codes (`-1` generic, `-2` panic, `-3` marshalling) must never gain typed -/// cases and must fall through to the generic brand error on throwing paths. -#[test] -fn negative_runtime_codes_fall_through_to_brand_error() { - let api = make_api(vec![Module { - name: "calc".into(), - functions: vec![ - Function { - throws: true, - ..plain_fn( - "div", - vec![p("a", TypeRef::I32), p("b", TypeRef::I32)], - Some(TypeRef::I32), - ) - }, - plain_fn("add", vec![p("a", TypeRef::I32)], Some(TypeRef::I32)), - Function { - r#async: true, - throws: true, - ..plain_fn("adiv", vec![], Some(TypeRef::I32)) - }, - Function { - r#async: true, - ..plain_fn("aadd", vec![], Some(TypeRef::I32)) - }, - ], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - interfaces: vec![], - errors: Some(ErrorDomain { - name: "CalcError".into(), - codes: vec![ErrorCode { - name: "DivByZero".into(), - code: 1, - message: "division by zero".into(), - doc: None, - fields: vec![], - }], - }), - modules: vec![], - }]); - let out = render(&api, "weaveffi", true, "weaveffi.yml", "WeaveFFI.swift"); - - // The mapper carries one arm per declared (positive) code and nothing - // else: no negative arm can shadow the runtime's reserved range. - let map = &out[out.find("func mapCalc(").expect("mapCalc emitted")..]; - let map = &map[..map.find("\n}").expect("mapCalc body ends")]; - assert!( - map.contains("case 1: return CalcError.divByZero("), - "declared code must map to the typed case: {map}" - ); - assert!( - !map.contains("case -"), - "no negative code may gain a typed case: {map}" - ); - assert!( - map.contains("default: return WeaveFFIError.error(code: code, message: message)"), - "unknown (negative runtime) codes must fall through to the brand error: {map}" - ); - - // Throwing sync path: codes route through the domain checker, whose - // fallback is the brand error above. - let div = &out[out.find("func div(").expect("div body")..]; - assert!( - div.contains("try checkCalc(&err)"), - "throwing fn must check through the domain mapper: {out}" - ); - // Non-throwing sync path: any reported code (necessarily a runtime code) - // traps. - let add = &out[out.find("func add(").expect("add body")..]; - assert!( - add[..add.find("\n }").unwrap_or(add.len())].contains("trap(&err)"), - "non-throwing fn must trap: {out}" - ); - // Throwing async path: the completion callback resumes with the mapped - // error (brand error for negative codes). - let adiv = &out[out.find("func adiv(").expect("adiv body")..]; - assert!( - adiv.contains( - "contRef.value.resume(throwing: mapCalc(code: code, message: msg, payload: payload))" - ), - "throwing async fn must resume with the mapped error: {out}" - ); - // Non-throwing async path: the completion callback traps. - let aadd = &out[out.find("func aadd(").expect("aadd body")..]; - assert!( - aadd.contains("fatalError(\"\\(code): \\(msg)\")"), - "non-throwing async fn must trap on a reported error: {out}" - ); -} diff --git a/crates/weaveffi-gen-swift/src/types.rs b/crates/weaveffi-gen-swift/src/types.rs index b761ddaf..ccb51dda 100644 --- a/crates/weaveffi-gen-swift/src/types.rs +++ b/crates/weaveffi-gen-swift/src/types.rs @@ -6,9 +6,9 @@ use std::collections::{HashMap, HashSet}; use heck::{ToLowerCamelCase, ToUpperCamelCase}; use weaveffi_core::abi::lower::split_qualified; use weaveffi_core::lang; +use weaveffi_core::model::Ty; use weaveffi_core::model::{EnumBinding, IteratorBinding}; use weaveffi_core::utils::local_type_name; -use weaveffi_ir::ir::TypeRef; /// The Swift spelling of a user-chosen identifier in a lowerCamel position /// (parameters, fields, enum cases, wrapper names): camel-cased, then @@ -29,36 +29,34 @@ pub(crate) fn swift_str(s: &str) -> String { /// /// Panics on an unresolved named reference or a non-return iterator, both of /// which validation rejects before rendering. -pub(crate) fn swift_type_for(t: &TypeRef) -> String { +pub(crate) fn swift_type_for(t: &Ty) -> String { match t { - TypeRef::I8 => "Int8".to_string(), - TypeRef::I16 => "Int16".to_string(), - TypeRef::I32 => "Int32".to_string(), - TypeRef::U8 => "UInt8".to_string(), - TypeRef::U16 => "UInt16".to_string(), - TypeRef::U32 => "UInt32".to_string(), - TypeRef::U64 => "UInt64".to_string(), - TypeRef::I64 => "Int64".to_string(), - TypeRef::F32 => "Float".to_string(), - TypeRef::F64 => "Double".to_string(), - TypeRef::Bool => "Bool".to_string(), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "String".to_string(), - TypeRef::Bytes | TypeRef::BorrowedBytes => "Data".to_string(), + Ty::I8 => "Int8".to_string(), + Ty::I16 => "Int16".to_string(), + Ty::I32 => "Int32".to_string(), + Ty::U8 => "UInt8".to_string(), + Ty::U16 => "UInt16".to_string(), + Ty::U32 => "UInt32".to_string(), + Ty::U64 => "UInt64".to_string(), + Ty::I64 => "Int64".to_string(), + Ty::F32 => "Float".to_string(), + Ty::F64 => "Double".to_string(), + Ty::Bool => "Bool".to_string(), + Ty::StringUtf8 | Ty::BorrowedStr => "String".to_string(), + Ty::Bytes | Ty::BorrowedBytes => "Data".to_string(), // Handles, plain and typed alike, are opaque `u64` resource tokens in // the wire format; Swift surfaces both as `UInt64` and converts to // the typed C pointer at the direct ABI boundary. - TypeRef::Handle | TypeRef::TypedHandle(_) => "UInt64".to_string(), - TypeRef::Enum(name) - | TypeRef::Record(name) - | TypeRef::RichEnum(name) - | TypeRef::Interface(name) => local_type_name(name).to_string(), - TypeRef::Named(_) => unreachable!("unresolved type reference"), - TypeRef::Optional(inner) => format!("{}?", swift_type_for(inner)), - TypeRef::List(inner) => format!("[{}]", swift_type_for(inner)), - TypeRef::Map(k, v) => format!("[{}: {}]", swift_type_for(k), swift_type_for(v)), + Ty::Handle | Ty::TypedHandle(_) => "UInt64".to_string(), + Ty::Enum(name) | Ty::Record(name) | Ty::RichEnum(name) | Ty::Interface(name) => { + local_type_name(name).to_string() + } + Ty::Optional(inner) => format!("{}?", swift_type_for(inner)), + Ty::List(inner) => format!("[{}]", swift_type_for(inner)), + Ty::Map(k, v) => format!("[{}: {}]", swift_type_for(k), swift_type_for(v)), // An iterator return renders as its per-function sequence class (see // `render_swift_iterator_class`), never through this generic mapping. - TypeRef::Iterator(_) => unreachable!("iterator type is only valid as a function return"), + Ty::Iterator(_) => unreachable!("iterator type is only valid as a function return"), } } @@ -102,15 +100,14 @@ impl SwiftCtx<'_> { /// Like [`swift_type_for`] but disambiguates wrapper-type names that collide /// with a module namespace (see [`SwiftCtx::ty_name`]). -pub(crate) fn swift_type_ctx(t: &TypeRef, ctx: SwiftCtx) -> String { +pub(crate) fn swift_type_ctx(t: &Ty, ctx: SwiftCtx) -> String { match t { - TypeRef::Record(name) - | TypeRef::RichEnum(name) - | TypeRef::Enum(name) - | TypeRef::Interface(name) => ctx.ty_name(local_type_name(name)), - TypeRef::Optional(inner) => format!("{}?", swift_type_ctx(inner, ctx)), - TypeRef::List(inner) => format!("[{}]", swift_type_ctx(inner, ctx)), - TypeRef::Map(k, v) => format!("[{}: {}]", swift_type_ctx(k, ctx), swift_type_ctx(v, ctx)), + Ty::Record(name) | Ty::RichEnum(name) | Ty::Enum(name) | Ty::Interface(name) => { + ctx.ty_name(local_type_name(name)) + } + Ty::Optional(inner) => format!("{}?", swift_type_ctx(inner, ctx)), + Ty::List(inner) => format!("[{}]", swift_type_ctx(inner, ctx)), + Ty::Map(k, v) => format!("[{}: {}]", swift_type_ctx(k, ctx), swift_type_ctx(v, ctx)), _ => swift_type_for(t), } } @@ -146,8 +143,8 @@ pub(crate) fn iterator_class_name(it: &IteratorBinding, c_prefix: &str) -> Strin /// Swift literal initializing the by-value `out_item` slot used while pulling /// from an iterator whose element lowers to a C value type. -pub(crate) fn swift_scalar_default(ty: &TypeRef) -> String { - if matches!(ty, TypeRef::Bool) { +pub(crate) fn swift_scalar_default(ty: &Ty) -> String { + if matches!(ty, Ty::Bool) { "false".to_string() } else { "0".to_string() diff --git a/crates/weaveffi-gen-wasm/src/calls.rs b/crates/weaveffi-gen-wasm/src/calls.rs index 2d5756d6..fb8435b7 100644 --- a/crates/weaveffi-gen-wasm/src/calls.rs +++ b/crates/weaveffi-gen-wasm/src/calls.rs @@ -5,18 +5,18 @@ //! each result's receiving contract from [`plan::ret_pass`], and each //! iterator element's release plan from [`plan::elem_free`], so this module //! only spells those shared contracts in JavaScript; it never re-derives -//! them from `TypeRef` shapes. +//! them from `Ty` shapes. use heck::ToLowerCamelCase; -use weaveffi_core::abi::{is_buffered, CType}; +use weaveffi_core::abi::CType; use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ BindingModel, CallShape, CallbackBinding, FnBinding, IteratorBinding, ListenerBinding, ModuleBinding, ParamBinding, }; use weaveffi_core::plan::{self, ArgPass, ElemFree, RetPass}; use weaveffi_core::utils::local_type_name; -use weaveffi_ir::ir::TypeRef; use crate::codec::{buf_read_expr, emit_buf_write_stmts}; use crate::docs::emit_doc; @@ -25,14 +25,14 @@ use crate::types::{js_checker_name, js_err_factory, js_fn_name, js_param_name, J /// The byte size of the linear-memory slot an iterator `next` writes one /// element of `ty` into: 8 for a `ptr` + `len` pair (bytes and buffered /// values), pointer or scalar width otherwise. -fn iter_slot_size(ty: &TypeRef) -> u32 { +fn iter_slot_size(ty: &Ty) -> u32 { match plan::elem_free(ty) { ElemFree::Bytes => 8, ElemFree::String => 4, ElemFree::None => match ty { - TypeRef::Bool | TypeRef::I8 | TypeRef::U8 => 1, - TypeRef::I16 | TypeRef::U16 => 2, - TypeRef::I64 | TypeRef::U64 | TypeRef::F64 | TypeRef::Handle => 8, + Ty::Bool | Ty::I8 | Ty::U8 => 1, + Ty::I16 | Ty::U16 => 2, + Ty::I64 | Ty::U64 | Ty::F64 | Ty::Handle => 8, _ => 4, }, } @@ -40,19 +40,19 @@ fn iter_slot_size(ty: &TypeRef) -> u32 { /// A JS expression reading one by-value scalar of `ty` from `DataView` `dv` /// at byte offset `at`. -fn read_scalar_at(ty: &TypeRef, dv: &str, at: &str) -> String { +fn read_scalar_at(ty: &Ty, dv: &str, at: &str) -> String { match ty { - TypeRef::Bool => format!("{dv}.getUint8({at}) !== 0"), - TypeRef::I8 => format!("{dv}.getInt8({at})"), - TypeRef::U8 => format!("{dv}.getUint8({at})"), - TypeRef::I16 => format!("{dv}.getInt16({at}, true)"), - TypeRef::U16 => format!("{dv}.getUint16({at}, true)"), - TypeRef::U32 => format!("{dv}.getUint32({at}, true)"), - TypeRef::I32 | TypeRef::Enum(_) => format!("{dv}.getInt32({at}, true)"), - TypeRef::I64 => format!("{dv}.getBigInt64({at}, true)"), - TypeRef::U64 | TypeRef::Handle => format!("{dv}.getBigUint64({at}, true)"), - TypeRef::F32 => format!("{dv}.getFloat32({at}, true)"), - TypeRef::F64 => format!("{dv}.getFloat64({at}, true)"), + Ty::Bool => format!("{dv}.getUint8({at}) !== 0"), + Ty::I8 => format!("{dv}.getInt8({at})"), + Ty::U8 => format!("{dv}.getUint8({at})"), + Ty::I16 => format!("{dv}.getInt16({at}, true)"), + Ty::U16 => format!("{dv}.getUint16({at}, true)"), + Ty::U32 => format!("{dv}.getUint32({at}, true)"), + Ty::I32 | Ty::Enum(_) => format!("{dv}.getInt32({at}, true)"), + Ty::I64 => format!("{dv}.getBigInt64({at}, true)"), + Ty::U64 | Ty::Handle => format!("{dv}.getBigUint64({at}, true)"), + Ty::F32 => format!("{dv}.getFloat32({at}, true)"), + Ty::F64 => format!("{dv}.getFloat64({at}, true)"), // Opaque pointers (typed handles, interfaces) are i32 slots. _ => format!("{dv}.getUint32({at}, true)"), } @@ -60,10 +60,10 @@ fn read_scalar_at(ty: &TypeRef, dv: &str, at: &str) -> String { /// A direct JS call argument for a scalar/handle value (coercing bool to 0/1 /// and 64-bit values to `BigInt` as the wasm calling convention requires). -fn js_arg_scalar(ty: &TypeRef, val: &str) -> String { +fn js_arg_scalar(ty: &Ty, val: &str) -> String { match ty { - TypeRef::Bool => format!("{val} ? 1 : 0"), - TypeRef::I64 | TypeRef::U64 | TypeRef::Handle => format!("BigInt({val})"), + Ty::Bool => format!("{val} ? 1 : 0"), + Ty::I64 | Ty::U64 | Ty::Handle => format!("BigInt({val})"), _ => val.to_string(), } } @@ -138,7 +138,7 @@ pub(crate) fn emit_stage_input( pub(crate) fn emit_return_decode( out: &mut String, indent: &str, - ret: Option<&TypeRef>, + ret: Option<&Ty>, symbol: &str, in_args: &[String], cleanup: &[String], @@ -183,11 +183,11 @@ pub(crate) fn emit_return_decode( /// /// Panics when `ret` is not an interface or optional interface, which cannot /// happen for a return classified as [`RetPass::Object`]. -fn object_ret_name(ret: &TypeRef) -> &str { +fn object_ret_name(ret: &Ty) -> &str { match ret { - TypeRef::Interface(name) => name, - TypeRef::Optional(inner) => match inner.as_ref() { - TypeRef::Interface(name) => name, + Ty::Interface(name) => name, + Ty::Optional(inner) => match inner.as_ref() { + Ty::Interface(name) => name, _ => unreachable!("non-interface optionals are buffered"), }, _ => unreachable!("only interface returns classify as RetPass::Object"), @@ -203,7 +203,7 @@ fn object_ret_name(ret: &TypeRef) -> &str { fn emit_decode_value( out: &mut String, indent: &str, - ret: Option<&TypeRef>, + ret: Option<&Ty>, r: &str, module: &str, prefix: &str, @@ -227,7 +227,7 @@ fn emit_decode_value( w.line("_rd.end();"); w.line("return _out;"); } - RetPass::Direct if matches!(ret, TypeRef::Bool) => { + RetPass::Direct if matches!(ret, Ty::Bool) => { w.line(format!("return {r} !== 0;")); } RetPass::Direct => { @@ -402,7 +402,7 @@ fn emit_cb_param_decode( )); w.line(format!("{target}_r.end();")); } - ArgPass::Direct { .. } if matches!(p.ty, TypeRef::Bool) => { + ArgPass::Direct { .. } if matches!(p.ty, Ty::Bool) => { w.line(format!("const {target} = {a} !== 0;")); } ArgPass::Direct { .. } => { @@ -586,9 +586,9 @@ fn emit_js_function_wrapper( /// `ptr` + `len` pair and freed with `free_bytes` (buffered elements are then /// decoded through the buffer reader), an interface pointer is adopted by /// `_wrap`, and a by-value element is read directly. -fn js_iter_decode_closure(elem: &TypeRef, module: &str) -> String { +fn js_iter_decode_closure(elem: &Ty, module: &str) -> String { match plan::elem_free(elem) { - ElemFree::Bytes if is_buffered(elem) => { + ElemFree::Bytes if elem.is_buffered() => { let read = buf_read_expr(elem, module, "_rd"); format!( "(w, p) => {{ const dv = new DataView(w.memory.buffer); const _rd = new _BufReader(_takeBytes(w, dv.getUint32(p, true), dv.getUint32(p + 4, true))); const _v = {read}; _rd.end(); return _v; }}" @@ -601,7 +601,7 @@ fn js_iter_decode_closure(elem: &TypeRef, module: &str) -> String { "(w, p) => _takeCStr(w, new DataView(w.memory.buffer).getUint32(p, true))".into() } ElemFree::None => match elem { - TypeRef::Interface(name) => { + Ty::Interface(name) => { let cls = local_type_name(name); format!("(w, p) => {cls}._wrap(new DataView(w.memory.buffer).getUint32(p, true))") } @@ -704,51 +704,50 @@ fn emit_js_iterator_function_wrapper( /// return: always `(ctx i32, err i32, ...result)`. Pointers are i32 on /// wasm32; only `i64`/`u64` widen to i64; a buffered result arrives as a /// borrowed `ptr` + `len` pair (two i32 slots). -pub(crate) fn async_cb_wasm_params(returns: Option<&TypeRef>) -> Vec<&'static str> { +pub(crate) fn async_cb_wasm_params(returns: Option<&Ty>) -> Vec<&'static str> { let mut params = vec!["i32", "i32"]; let Some(ty) = returns else { return params; }; - if is_buffered(ty) { + if ty.is_buffered() { params.push("i32"); params.push("i32"); return params; } match ty { - TypeRef::I8 - | TypeRef::I16 - | TypeRef::I32 - | TypeRef::U8 - | TypeRef::U16 - | TypeRef::U32 - | TypeRef::Bool - | TypeRef::Enum(_) - | TypeRef::StringUtf8 - | TypeRef::BorrowedStr - | TypeRef::Interface(_) - | TypeRef::TypedHandle(_) - | TypeRef::Iterator(_) + Ty::I8 + | Ty::I16 + | Ty::I32 + | Ty::U8 + | Ty::U16 + | Ty::U32 + | Ty::Bool + | Ty::Enum(_) + | Ty::StringUtf8 + | Ty::BorrowedStr + | Ty::Interface(_) + | Ty::TypedHandle(_) + | Ty::Iterator(_) // Only `Interface?` reaches here: a nullable object pointer. - | TypeRef::Optional(_) => { + | Ty::Optional(_) => { params.push("i32"); } - TypeRef::I64 | TypeRef::U64 | TypeRef::Handle => { + Ty::I64 | Ty::U64 | Ty::Handle => { params.push("i64"); } - TypeRef::F32 => { + Ty::F32 => { params.push("f32"); } - TypeRef::F64 => { + Ty::F64 => { params.push("f64"); } - TypeRef::Bytes | TypeRef::BorrowedBytes => { + Ty::Bytes | Ty::BorrowedBytes => { params.push("i32"); params.push("i32"); } - TypeRef::Record(_) | TypeRef::RichEnum(_) | TypeRef::List(_) | TypeRef::Map(_, _) => { + Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => { unreachable!("buffered type handled above") } - TypeRef::Named(_) => unreachable!("unresolved type reference"), } params } @@ -768,7 +767,7 @@ pub(crate) fn async_cb_wasm_params(returns: Option<&TypeRef>) -> Vec<&'static st fn emit_async_unwrap( out: &mut String, indent: &str, - ret: Option<&TypeRef>, + ret: Option<&Ty>, mk_err: Option<&str>, module: &str, prefix: &str, @@ -801,7 +800,7 @@ fn emit_async_unwrap( w.line("return _v;"); }); } - RetPass::Direct if matches!(ret, TypeRef::Bool) => { + RetPass::Direct if matches!(ret, Ty::Bool) => { w.line(format!("{open}(w, r) => r !== 0 }});")); } RetPass::Direct => { diff --git a/crates/weaveffi-gen-wasm/src/codec.rs b/crates/weaveffi-gen-wasm/src/codec.rs index f4219220..e3660bdf 100644 --- a/crates/weaveffi-gen-wasm/src/codec.rs +++ b/crates/weaveffi-gen-wasm/src/codec.rs @@ -13,8 +13,8 @@ use weaveffi_core::abi::lower::split_qualified; use weaveffi_core::codegen::CodeWriter; use weaveffi_core::errors::ERROR_BRAND; use weaveffi_core::model::BindingModel; -use weaveffi_core::wire::{self, WireType}; -use weaveffi_ir::ir::TypeRef; +use weaveffi_core::model::Ty; +use weaveffi_core::model::{Prim, WireType}; /// The `_write_*`/`_read_*` codec function names for a (possibly /// `module.Name`-qualified) record or rich enum referenced from @@ -36,19 +36,19 @@ pub(crate) fn buf_codec_names(name: &str, current_module: &str) -> (String, Stri /// and containers before falling through to a leaf method. fn scalar_method(wt: WireType<'_>) -> &'static str { match wt { - WireType::Bool => "bool", - WireType::I8 => "i8", - WireType::U8 => "u8", - WireType::I16 => "i16", - WireType::U16 => "u16", - WireType::I32 => "i32", - WireType::U32 => "u32", - WireType::I64 => "i64", - WireType::U64 | WireType::Handle => "u64", - WireType::F32 => "f32", - WireType::F64 => "f64", - WireType::String => "str", - WireType::Bytes => "bytes", + WireType::Prim(Prim::Bool) => "bool", + WireType::Prim(Prim::I8) => "i8", + WireType::Prim(Prim::U8) => "u8", + WireType::Prim(Prim::I16) => "i16", + WireType::Prim(Prim::U16) => "u16", + WireType::Prim(Prim::I32) => "i32", + WireType::Prim(Prim::U32) => "u32", + WireType::Prim(Prim::I64) => "i64", + WireType::Prim(Prim::U64) | WireType::Handle(_) => "u64", + WireType::Prim(Prim::F32) => "f32", + WireType::Prim(Prim::F64) => "f64", + WireType::Prim(Prim::String) => "str", + WireType::Prim(Prim::Bytes) => "bytes", other => unreachable!("composite wire shape dispatched by the caller: {other:?}"), } } @@ -58,13 +58,13 @@ fn scalar_method(wt: WireType<'_>) -> &'static str { /// references against `module`. `tmp` supplies collision-free local names. pub(crate) fn emit_buf_write_stmts( w: &mut CodeWriter, - ty: &TypeRef, + ty: &Ty, wtr: &str, val: &str, module: &str, tmp: &mut u32, ) { - match wire::classify(ty) { + match ty.wire() { WireType::Enum(_) => { w.line(format!("{wtr}.i32({val});")); } @@ -124,11 +124,11 @@ pub(crate) fn emit_buf_write_stmts( /// named `rdr`, resolving record and rich-enum references against `module`. /// Composite types recurse; lists and maps expand to inline arrow IIFEs so /// the whole decode stays a single expression. -pub(crate) fn buf_read_expr(ty: &TypeRef, module: &str, rdr: &str) -> String { - match wire::classify(ty) { +pub(crate) fn buf_read_expr(ty: &Ty, module: &str, rdr: &str) -> String { + match ty.wire() { // A typed handle is an i32 pointer at the ABI but a u64 on the wire; // narrowing back to a JS number keeps the two spellings interchangeable. - WireType::Handle if matches!(ty, TypeRef::TypedHandle(_)) => { + WireType::Handle(_) if matches!(ty, Ty::TypedHandle(_)) => { format!("Number({rdr}.u64())") } WireType::Enum(_) => format!("{rdr}.i32()"), diff --git a/crates/weaveffi-gen-wasm/src/dts.rs b/crates/weaveffi-gen-wasm/src/dts.rs index 7acf9718..e8ffd55a 100644 --- a/crates/weaveffi-gen-wasm/src/dts.rs +++ b/crates/weaveffi-gen-wasm/src/dts.rs @@ -2,18 +2,15 @@ //! record and enum type shapes, ambient interface classes, and the nested //! module interface the loader's promise resolves to. -use std::collections::HashMap; - use heck::{ToLowerCamelCase, ToUpperCamelCase}; -use weaveffi_core::codegen::common::{walk_modules_with_path, DocCommentStyle}; +use weaveffi_core::codegen::common::DocCommentStyle; use weaveffi_core::codegen::CodeWriter; use weaveffi_core::errors::ERROR_BRAND; use weaveffi_core::model::{ - BindingModel, CallShape, ErrorBinding, FnBinding, InterfaceBinding, ListenerBinding, - ModuleBinding, + BindingModel, CallShape, EnumBinding, ErrorBinding, FnBinding, InterfaceBinding, + ListenerBinding, ModuleBinding, }; use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; -use weaveffi_ir::ir::{Api, EnumDef, Module}; use crate::docs::{emit_doc, emit_fn_doc}; use crate::types::{js_code_class_name, js_fn_name, js_param_name, ts_type_for}; @@ -24,7 +21,6 @@ use crate::types::{js_code_class_name, js_fn_name, js_param_name, ts_type_for}; /// members and listeners are omitted in Emscripten mode, turning their /// runtime stubs into compile-time errors for TS consumers. pub(crate) fn render_wasm_dts( - api: &Api, model: &BindingModel, module_name: &str, input_basename: &str, @@ -34,15 +30,13 @@ pub(crate) fn render_wasm_dts( let pascal_name = module_name.to_upper_camel_case(); let interface_name = format!("{pascal_name}Module"); let load_fn = format!("load{pascal_name}"); - let by_path: HashMap<&str, &ModuleBinding> = - model.modules.iter().map(|m| (m.path.as_str(), m)).collect(); let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); out.push_str("// Generated TypeScript declarations for WeaveFFI Wasm bindings\n\n"); emit_dts_error_classes(&mut out, model); - for (m, path) in walk_modules_with_path(&api.modules) { - for s in &m.structs { + for mb in &model.modules { + for s in &mb.structs { emit_doc(&mut out, &s.doc, ""); out.push_str(&format!("export interface {} {{\n", s.name)); for field in &s.fields { @@ -52,7 +46,7 @@ pub(crate) fn render_wasm_dts( out.push_str("}\n\n"); } - for e in &m.enums { + for e in &mb.enums { // A rich (algebraic) enum is a tagged plain-object union, not a // by-value discriminant constant. if e.is_rich() { @@ -60,18 +54,22 @@ pub(crate) fn render_wasm_dts( continue; } emit_doc(&mut out, &e.doc, ""); + // The const object holds the values; the same-named type alias + // is their union, so `Mode` works in both value and type positions. out.push_str(&format!("export declare const {}: Readonly<{{\n", e.name)); for v in &e.variants { emit_doc(&mut out, &v.doc, " "); out.push_str(&format!(" {}: {};\n", v.name, v.value)); } - out.push_str("}>;\n\n"); + out.push_str("}>;\n"); + out.push_str(&format!( + "export type {0} = (typeof {0})[keyof typeof {0}];\n\n", + e.name + )); } - if let Some(mb) = by_path.get(path.as_str()) { - for i in &mb.interfaces { - emit_dts_interface_class(&mut out, mb, i, emscripten); - } + for i in &mb.interfaces { + emit_dts_interface_class(&mut out, mb, i, emscripten); } } @@ -88,8 +86,8 @@ pub(crate) fn render_wasm_dts( } else { out.push_str(" _raw: WebAssembly.Exports;\n"); } - for module in &api.modules { - render_dts_module_interface(&mut out, module, &module.name, &by_path, " ", emscripten); + for module in model.roots() { + render_dts_module_interface(&mut out, model, module, " ", emscripten); } } out.push_str("}\n\n"); @@ -111,7 +109,7 @@ pub(crate) fn render_wasm_dts( /// discriminated union of plain object shapes, one member per variant, keyed /// by the string `tag`. Mirrors the runtime representation the buffer codecs /// pack and unpack. -fn emit_dts_rich_enum_type(out: &mut String, e: &EnumDef) { +fn emit_dts_rich_enum_type(out: &mut String, e: &EnumBinding) { let name = &e.name; let mut w = CodeWriter::two_space(); w.doc(&e.doc, DocCommentStyle::Javadoc); @@ -193,39 +191,25 @@ fn dts_fn_tags(f: &FnBinding, error: Option<&ErrorBinding>) -> Vec { /// content. fn render_dts_module_interface( out: &mut String, - m: &Module, - module_path: &str, - by_path: &HashMap<&str, &ModuleBinding>, + model: &BindingModel, + mb: &ModuleBinding, indent: &str, emscripten: bool, ) { - fn tree_has_content( - m: &Module, - path: &str, - by_path: &HashMap<&str, &ModuleBinding>, - include_listeners: bool, - ) -> bool { - let here = by_path.get(path).is_some_and(|mb| { - !mb.functions.is_empty() - || !mb.interfaces.is_empty() - || (include_listeners && !mb.listeners.is_empty()) - }); - here || m.modules.iter().any(|sub| { - tree_has_content( - sub, - &format!("{path}_{}", sub.name), - by_path, - include_listeners, - ) - }) + fn tree_has_content(model: &BindingModel, mb: &ModuleBinding, include_listeners: bool) -> bool { + !mb.functions.is_empty() + || !mb.interfaces.is_empty() + || (include_listeners && !mb.listeners.is_empty()) + || model + .children(mb) + .any(|sub| tree_has_content(model, sub, include_listeners)) } - if !tree_has_content(m, module_path, by_path, !emscripten) { + if !tree_has_content(model, mb, !emscripten) { return; } - let mb = by_path[module_path]; let error = mb.error.as_ref(); let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - w.block(format!("{}: {{", m.name), "};", |w| { + w.block(format!("{}: {{", mb.name), "};", |w| { let inner = w.indent_str(); for f in &mb.functions { // Async functions are throwing stubs in Emscripten mode; omitting @@ -257,10 +241,9 @@ fn render_dts_module_interface( for i in &mb.interfaces { w.line(format!("{}: typeof {};", i.name, i.name)); } - for sub in &m.modules { - let sub_path = format!("{module_path}_{}", sub.name); + for sub in model.children(mb) { let mut tmp = String::new(); - render_dts_module_interface(&mut tmp, sub, &sub_path, by_path, &inner, emscripten); + render_dts_module_interface(&mut tmp, model, sub, &inner, emscripten); w.raw(tmp); } }); diff --git a/crates/weaveffi-gen-wasm/src/entities.rs b/crates/weaveffi-gen-wasm/src/entities.rs index 0830bc81..b92e3655 100644 --- a/crates/weaveffi-gen-wasm/src/entities.rs +++ b/crates/weaveffi-gen-wasm/src/entities.rs @@ -2,16 +2,13 @@ //! runtime, module-scope enum and error surfaces, loader-scoped interface //! classes, and the nested API object the loader returns. -use std::collections::HashMap; - use heck::ToUpperCamelCase; -use weaveffi_core::abi::is_buffered; use weaveffi_core::codegen::CodeWriter; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ BindingModel, CallShape, CallbackBinding, InterfaceBinding, ModuleBinding, }; use weaveffi_core::utils::{render_prelude, render_trailer, CommentStyle}; -use weaveffi_ir::ir::{Api, Module, TypeRef}; use crate::calls::{ async_cb_wasm_params, collect_listener_callbacks, emit_js_callable, emit_js_listener_api, @@ -19,11 +16,11 @@ use crate::calls::{ }; use crate::codec::{emit_js_buffer_codecs, emit_js_buffer_runtime}; use crate::runtime::{ - emit_bytes_helpers, emit_check_err_ref, emit_error_slot_helpers, emit_iterator_class, - emit_js_error_checkers, emit_js_error_classes, emit_string_helpers, emit_trampoline_helper, + emit_abi_version_check, emit_bytes_helpers, emit_check_err_ref, emit_error_slot_helpers, + emit_iterator_class, emit_js_error_checkers, emit_js_error_classes, emit_string_helpers, + emit_trampoline_helper, }; -use crate::types::{api_deep_any, is_string_type, js_checker_name, js_param_name, JsDecl}; -use weaveffi_core::codegen::common::walk_modules_with_path; +use crate::types::{is_string_type, js_checker_name, js_param_name, JsDecl}; /// Every producer entry-point symbol the generated glue calls by name: /// sync and iterator launchers, iterator `next`/destroy pairs, and interface @@ -62,7 +59,6 @@ pub(crate) fn collect_called_symbols(model: &BindingModel) -> Vec { /// Emscripten mode, adopts) the wasm module and returns the nested API /// object. pub(crate) fn render_wasm_js_stub( - api: &Api, model: &BindingModel, module_name: &str, prefix: &str, @@ -73,8 +69,6 @@ pub(crate) fn render_wasm_js_stub( let pascal_name = module_name.to_upper_camel_case(); let load_fn = format!("load{pascal_name}"); let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename); - let by_path: HashMap<&str, &ModuleBinding> = - model.modules.iter().map(|m| (m.path.as_str(), m)).collect(); // Interface members marshal like free functions, so every callable counts. let has_functions = model.modules.iter().any(|m| m.callables().next().is_some()); @@ -106,19 +100,16 @@ pub(crate) fn render_wasm_js_stub( .as_ref() .is_some_and(|e| e.declared_here && e.codes.iter().any(|c| !c.fields.is_empty())) }); - let needs_buf = has_codecs || has_error_payloads || api_deep_any(api, &is_buffered); + let needs_buf = has_codecs || has_error_payloads || model.any_type(&|t| t.is_buffered()); // The buffer reader (and the codecs) reject malformed input by throwing // the brand error, so the error surface is needed whenever buffers are. let needs_err = has_functions || needs_buf; // Error messages always cross as C strings, so anything needing the error // helpers also needs the string-read helpers regardless of declared types. - let needs_strings = needs_err || api_deep_any(api, &is_string_type); + let needs_strings = needs_err || model.any_type(&is_string_type); // Buffered values are staged and released exactly like bytes, so the byte // helpers cover both. - let needs_bytes = needs_buf - || api_deep_any(api, &|t| { - matches!(t, TypeRef::Bytes | TypeRef::BorrowedBytes) - }); + let needs_bytes = needs_buf || model.any_type(&|t| matches!(t, Ty::Bytes | Ty::BorrowedBytes)); // Any iterator-returning callable pulls in the shared lazy-iterator // wrapper class. let has_iterators = model @@ -144,6 +135,10 @@ pub(crate) fn render_wasm_js_stub( out.push_str("// as one value buffer: i32 pointer + i32 length\n"); out.push('\n'); + if !model.modules.is_empty() { + emit_abi_version_check(&mut out); + } + if needs_err { emit_js_error_classes(&mut out, model); } @@ -180,7 +175,7 @@ pub(crate) fn render_wasm_js_stub( emit_trampoline_helper(&mut out); } - for (module, _path) in walk_modules_with_path(&api.modules) { + for module in &model.modules { for e in &module.enums { // Rich (algebraic) enums are tagged plain-object unions handled by // the buffer codecs; only C-style enums surface as a by-value @@ -202,7 +197,7 @@ pub(crate) fn render_wasm_js_stub( out.push_str(" *\n"); out.push_str(" * @param {Object|Promise} module - The initialized Emscripten\n"); out.push_str(" * module, or the promise returned by its `MODULARIZE` factory.\n"); - if api.modules.is_empty() { + if model.modules.is_empty() { out.push_str(" * @returns {Promise} The Emscripten module.\n"); } else { out.push_str(" * @returns {Promise} The API bindings.\n"); @@ -211,7 +206,7 @@ pub(crate) fn render_wasm_js_stub( out.push_str(" * Load a WeaveFFI Wasm module from the given URL.\n"); out.push_str(" *\n"); out.push_str(" * @param {string} url - URL to the `.wasm` file.\n"); - if api.modules.is_empty() { + if model.modules.is_empty() { out.push_str( " * @returns {Promise} The exported Wasm functions.\n", ); @@ -259,7 +254,7 @@ pub(crate) fn render_wasm_js_stub( out.push_str(" const { instance } = await WebAssembly.instantiate(bytes, {});\n"); } - if api.modules.is_empty() { + if model.modules.is_empty() { if emscripten { out.push_str(" return m;\n"); } else { @@ -273,6 +268,10 @@ pub(crate) fn render_wasm_js_stub( // renaming cannot break it, while the rest of the glue keeps // consistent dot access on this locally-constructed object. let mut bindings: Vec<(String, String)> = vec![ + ( + "weaveffi_abi_version".to_string(), + format!("{prefix}_abi_version"), + ), ("weaveffi_alloc".to_string(), format!("{prefix}_alloc")), ("weaveffi_dealloc".to_string(), format!("{prefix}_dealloc")), ]; @@ -312,6 +311,7 @@ pub(crate) fn render_wasm_js_stub( } else { out.push_str(" const wasm = instance.exports;\n\n"); } + out.push_str(" _checkAbiVersion(wasm);\n\n"); if has_async || has_listeners { out.push_str(" const _table = wasm.__indirect_function_table;\n\n"); @@ -377,16 +377,8 @@ pub(crate) fn render_wasm_js_stub( out.push_str(" return {\n"); out.push_str(" _raw: wasm,\n"); - for module in &api.modules { - render_js_module_object( - &mut out, - module, - &module.name, - &by_path, - " ", - prefix, - emscripten, - ); + for module in model.roots() { + render_js_module_object(&mut out, model, module, " ", prefix, emscripten); } out.push_str(" };\n"); } @@ -400,18 +392,13 @@ pub(crate) fn render_wasm_js_stub( /// classes, or listeners), so empty namespace objects are not emitted. /// Records and rich enums contribute nothing here: they are plain value /// shapes with no runtime members. -fn module_tree_has_content( - m: &Module, - path: &str, - by_path: &HashMap<&str, &ModuleBinding>, -) -> bool { - let here = by_path.get(path).is_some_and(|mb| { - !mb.functions.is_empty() || !mb.interfaces.is_empty() || !mb.listeners.is_empty() - }); - here || m - .modules - .iter() - .any(|sub| module_tree_has_content(sub, &format!("{path}_{}", sub.name), by_path)) +fn module_tree_has_content(model: &BindingModel, mb: &ModuleBinding) -> bool { + !mb.functions.is_empty() + || !mb.interfaces.is_empty() + || !mb.listeners.is_empty() + || model + .children(mb) + .any(|sub| module_tree_has_content(model, sub)) } /// Emit one module's namespace object (`math: { ... },`) inside the returned @@ -419,22 +406,19 @@ fn module_tree_has_content( /// (or their Emscripten stubs), the interface classes themselves (so /// factories, statics, and `instanceof` checks reach them), and nested /// submodule objects, skipping subtrees with no runtime content. -#[allow(clippy::too_many_arguments)] fn render_js_module_object( out: &mut String, - m: &Module, - module_path: &str, - by_path: &HashMap<&str, &ModuleBinding>, + model: &BindingModel, + mb: &ModuleBinding, indent: &str, prefix: &str, emscripten: bool, ) { - if !module_tree_has_content(m, module_path, by_path) { + if !module_tree_has_content(model, mb) { return; } - let mb = by_path[module_path]; let mut w = CodeWriter::two_space().with_depth(indent.len() / 2); - w.block(format!("{}: {{", m.name), "},", |w| { + w.block(format!("{}: {{", mb.name), "},", |w| { let inner = w.indent_str(); for f in &mb.functions { let mut tmp = String::new(); @@ -464,12 +448,9 @@ fn render_js_module_object( for i in &mb.interfaces { w.line(format!("{}: {},", i.name, i.name)); } - for sub in &m.modules { - let sub_path = format!("{module_path}_{}", sub.name); + for sub in model.children(mb) { let mut tmp = String::new(); - render_js_module_object( - &mut tmp, sub, &sub_path, by_path, &inner, prefix, emscripten, - ); + render_js_module_object(&mut tmp, model, sub, &inner, prefix, emscripten); w.raw(tmp); } }); diff --git a/crates/weaveffi-gen-wasm/src/lib.rs b/crates/weaveffi-gen-wasm/src/lib.rs index 3e5d3435..9fd4b7a7 100644 --- a/crates/weaveffi-gen-wasm/src/lib.rs +++ b/crates/weaveffi-gen-wasm/src/lib.rs @@ -18,9 +18,6 @@ mod package; mod runtime; mod types; -#[cfg(test)] -mod tests; - use camino::Utf8Path; use serde::{Deserialize, Serialize}; use weaveffi_core::backend::{LanguageBackend, OutputFile}; @@ -136,7 +133,6 @@ impl LanguageBackend for WasmGenerator { OutputFile::new( wasm_dir.join(&js_filename), render_wasm_js_stub( - api, model, module_name, prefix, @@ -148,7 +144,6 @@ impl LanguageBackend for WasmGenerator { OutputFile::new( wasm_dir.join(&dts_filename), render_wasm_dts( - api, model, module_name, input_basename, @@ -159,5 +154,3 @@ impl LanguageBackend for WasmGenerator { ] } } - -weaveffi_core::impl_generator_via_backend!(WasmGenerator); diff --git a/crates/weaveffi-gen-wasm/src/package.rs b/crates/weaveffi-gen-wasm/src/package.rs index 17216496..dc3c41fa 100644 --- a/crates/weaveffi-gen-wasm/src/package.rs +++ b/crates/weaveffi-gen-wasm/src/package.rs @@ -183,6 +183,7 @@ pub(crate) fn render_wasm_readme( out.push_str("16 bytes on wasm32: `{ i32 code, char* message, uint8_t* payload_ptr,\n"); out.push_str("size_t payload_len }`. Your Wasm module must export the following\n"); out.push_str("functions:\n\n"); + out.push_str("- `weaveffi_abi_version() -> i32`: the ABI revision the module implements; the loader refuses a mismatch\n"); out.push_str("- `weaveffi_alloc(size: i32) -> i32`: allocate `size` bytes in linear memory\n"); out.push_str("- `weaveffi_dealloc(ptr: i32, size: i32)`: release a `weaveffi_alloc` block\n"); out.push_str("- `weaveffi_error_clear(err_ptr: i32)`: clear and free error resources\n"); diff --git a/crates/weaveffi-gen-wasm/src/runtime.rs b/crates/weaveffi-gen-wasm/src/runtime.rs index 9916425f..6df8c365 100644 --- a/crates/weaveffi-gen-wasm/src/runtime.rs +++ b/crates/weaveffi-gen-wasm/src/runtime.rs @@ -6,6 +6,7 @@ //! [`crate::entities`], keyed off which features the API actually uses. use heck::ToShoutySnakeCase; +use weaveffi_core::cabi::ABI_VERSION; use weaveffi_core::codegen::CodeWriter; use weaveffi_core::errors::ERROR_BRAND; use weaveffi_core::model::{BindingModel, ErrorBinding}; @@ -267,6 +268,25 @@ pub(crate) fn emit_error_slot_helpers(out: &mut String) { out.push_str("}\n\n"); } +/// Emit `_checkAbiVersion`, which the loader calls on the bound export table +/// before returning the API object. A module that does not export +/// `weaveffi_abi_version` predates versioning; one reporting a different +/// revision was built against an incompatible runtime. Both throw so the +/// mismatch surfaces at load time rather than as a garbled value buffer. +pub(crate) fn emit_abi_version_check(out: &mut String) { + out.push_str("// The ABI revision this glue was generated against.\n"); + out.push_str(&format!("const _ABI_VERSION = {ABI_VERSION};\n\n")); + out.push_str("function _checkAbiVersion(wasm) {\n"); + out.push_str(" if (typeof wasm.weaveffi_abi_version !== 'function') {\n"); + out.push_str(" throw new Error(`the loaded WeaveFFI module predates ABI versioning (this glue expects ABI revision ${_ABI_VERSION})`);\n"); + out.push_str(" }\n"); + out.push_str(" const found = wasm.weaveffi_abi_version() >>> 0;\n"); + out.push_str(" if (found !== _ABI_VERSION) {\n"); + out.push_str(" throw new Error(`WeaveFFI ABI mismatch: this glue expects revision ${_ABI_VERSION} but the loaded module reports revision ${found}`);\n"); + out.push_str(" }\n"); + out.push_str("}\n\n"); +} + /// Emit `_checkErrRef`, the boxed-error checker the async completion /// callbacks route through: the consumer owns the heap-boxed error struct, /// so its fields are read (and any typed error built) before the box is diff --git a/crates/weaveffi-gen-wasm/src/tests.rs b/crates/weaveffi-gen-wasm/src/tests.rs deleted file mode 100644 index 46c68edd..00000000 --- a/crates/weaveffi-gen-wasm/src/tests.rs +++ /dev/null @@ -1,2859 +0,0 @@ -//! Unit tests: golden-content assertions over the JS loader, the -//! TypeScript declarations, the README, and the package manifest. - -use super::*; -use camino::Utf8Path; -use weaveffi_core::codegen::Generator; -use weaveffi_core::resolved::ResolvedApi; -use weaveffi_ir::ir::Api; -use weaveffi_ir::ir::{ - EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField, TypeRef, -}; - -use crate::dts::render_wasm_dts; -use crate::entities::render_wasm_js_stub; -use crate::package::render_wasm_readme; -use crate::types::{ts_type_for, type_display, wasm_type, wasm_type_note}; - -fn empty_api() -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules: vec![], - generators: None, - package: None, - }) -} - -fn make_api(modules: Vec) -> ResolvedApi { - ResolvedApi::assume_resolved(Api { - version: "0.7.0".into(), - modules, - generators: None, - package: None, - }) -} - -/// Test-only shim: build the model (the driver's job in production) and -/// render the JS stub with the historical argument order. -fn js_stub_for( - api: &ResolvedApi, - module_name: &str, - prefix: &str, - input_basename: &str, - filename: &str, - emscripten: bool, -) -> String { - let model = BindingModel::build(api, prefix); - render_wasm_js_stub( - api, - &model, - module_name, - prefix, - input_basename, - filename, - emscripten, - ) -} - -/// Test-only shim mirroring [`js_stub_for`] for the `.d.ts` renderer. -fn dts_for( - api: &ResolvedApi, - module_name: &str, - input_basename: &str, - filename: &str, - emscripten: bool, -) -> String { - let model = BindingModel::build(api, "weaveffi"); - render_wasm_dts( - api, - &model, - module_name, - input_basename, - filename, - emscripten, - ) -} - -/// Test-only shim mirroring [`js_stub_for`] for the README renderer. -fn readme_for(api: &ResolvedApi, prefix: &str, input_basename: &str, emscripten: bool) -> String { - let model = BindingModel::build(api, prefix); - render_wasm_readme(api, &model, prefix, input_basename, emscripten) -} - -fn field(name: &str, ty: TypeRef) -> StructField { - StructField { - name: name.into(), - ty, - doc: None, - } -} - -fn member( - name: &str, - params: Vec, - returns: Option, - throws: bool, - is_async: bool, -) -> Function { - Function { - name: name.into(), - params, - returns, - doc: None, - throws, - r#async: is_async, - cancellable: is_async, - deprecated: None, - since: None, - } -} - -fn param(name: &str, ty: TypeRef) -> Param { - Param { - name: name.into(), - ty, - mutable: false, - doc: None, - } -} - -fn str_param(name: &str) -> Param { - param(name, TypeRef::StringUtf8) -} - -fn module(name: &str) -> Module { - Module { - name: name.into(), - functions: vec![], - structs: vec![], - enums: vec![], - callbacks: vec![], - listeners: vec![], - errors: None, - interfaces: vec![], - modules: vec![], - } -} - -fn sample_api() -> ResolvedApi { - make_api(vec![Module { - functions: vec![Function { - name: "add".into(), - params: vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - returns: Some(TypeRef::I32), - doc: Some("Add two numbers".into()), - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Point".into(), - doc: Some("A 2D point".into()), - fields: vec![field("x", TypeRef::F64), field("y", TypeRef::F64)], - }], - enums: vec![EnumDef { - name: "Color".into(), - doc: Some("Primary colors".into()), - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Blue".into(), - value: 2, - doc: None, - fields: vec![], - }, - ], - }], - ..module("math") - }]) -} - -/// An API with a callback + listener, delivered synchronously through a -/// long-lived function-table trampoline in the standard loader (and -/// stubbed in Emscripten mode). -fn listener_api() -> ResolvedApi { - make_api(vec![Module { - functions: vec![member("send", vec![str_param("text")], None, false, false)], - callbacks: vec![weaveffi_ir::ir::CallbackDef { - name: "OnMessage".into(), - params: vec![str_param("message")], - doc: None, - }], - listeners: vec![weaveffi_ir::ir::ListenerDef { - name: "message_listener".into(), - event_callback: "OnMessage".into(), - doc: None, - }], - ..module("events") - }]) -} - -#[test] -fn capabilities_declare_full_support() { - let caps = LanguageBackend::capabilities(&WasmGenerator); - assert_eq!(caps, TargetCapabilities::full()); -} - -#[test] -fn listeners_emit_register_unregister_in_js() { - let js = js_stub_for( - &listener_api(), - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - // One long-lived trampoline per callback typedef, in the function - // table, decoding the borrowed string argument without freeing it. - assert!( - js.contains( - "const _lsnPtr_weaveffi_events_OnMessage_fn = _registerTrampoline(_table, ['i32', 'i32']," - ), - "{js}" - ); - assert!(js.contains("const _p0 = _readCStr(wasm, a0);"), "{js}"); - assert!(js.contains("_l.callback(_p0);"), "{js}"); - // Register hands the trampoline and a context id to the producer and - // returns the numeric context id; unregister releases both sides. - assert!(js.contains("registerMessageListener(callback) {"), "{js}"); - assert!( - js.contains( - "wasm.weaveffi_events_register_message_listener(_lsnPtr_weaveffi_events_OnMessage_fn, _id)" - ), - "{js}" - ); - assert!(js.contains("unregisterMessageListener(id) {"), "{js}"); - assert!( - js.contains("wasm.weaveffi_events_unregister_message_listener(_l.rid);"), - "{js}" - ); - assert!(!js.contains("is not supported"), "{js}"); -} - -#[test] -fn listeners_declared_in_dts() { - let api = listener_api(); - let dts = dts_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi.yml", - "weaveffi_wasm.d.ts", - false, - ); - assert!( - dts.contains("registerMessageListener(callback: (message: string) => void): number;"), - "{dts}" - ); - assert!( - dts.contains("unregisterMessageListener(id: number): void;"), - "{dts}" - ); - assert!(dts.contains("send(text: string)"), "{dts}"); -} - -#[test] -fn readme_documents_listeners() { - let readme = readme_for(&listener_api(), "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("### Callbacks and Listeners"), "{readme}"); - assert!(readme.contains("synchronous"), "{readme}"); - assert!(readme.contains("subscription id"), "{readme}"); - assert!(readme.contains("buffered values"), "{readme}"); - assert!(!readme.contains("## Unsupported Features"), "{readme}"); -} - -#[test] -fn listener_free_api_has_no_listener_section() { - let readme = readme_for(&sample_api(), "weaveffi", "weaveffi.yml", false); - assert!(!readme.contains("### Callbacks and Listeners")); -} - -#[test] -fn listeners_emit_throwing_stubs_in_emscripten_mode() { - let js = js_stub_for( - &listener_api(), - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - true, - ); - assert!(js.contains("registerMessageListener() {"), "{js}"); - assert!(js.contains("unregisterMessageListener() {"), "{js}"); - assert!( - js.contains("listener 'message_listener' is not supported in Emscripten mode"), - "{js}" - ); - assert!( - !js.contains("_lsnPtr_") && !js.contains("_listeners"), - "no listener machinery in Emscripten mode: {js}" - ); -} - -#[test] -fn listeners_omitted_from_dts_in_emscripten_mode() { - let api = listener_api(); - let dts = dts_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi.yml", - "weaveffi_wasm.d.ts", - true, - ); - assert!(!dts.contains("registerMessageListener"), "{dts}"); - assert!(dts.contains("send(text: string)"), "{dts}"); -} - -#[test] -fn readme_documents_listener_gap_in_emscripten_mode() { - let readme = readme_for(&listener_api(), "weaveffi", "weaveffi.yml", true); - assert!(readme.contains("## Callbacks and Listeners"), "{readme}"); - assert!( - readme.contains("not supported in Emscripten mode"), - "{readme}" - ); -} - -#[test] -fn readme_documents_records_as_plain_objects() { - let readme = readme_for(&empty_api(), "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("### Records")); - assert!(readme.contains("plain JavaScript objects")); - assert!(readme.contains("value-buffer format")); - assert!(readme.contains("nothing to free")); - assert!(!readme.contains("opaque handles")); -} - -#[test] -fn readme_documents_enums() { - let readme = readme_for(&empty_api(), "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("### Enums")); - assert!(readme.contains("`i32` values")); - assert!(readme.contains("discriminant")); - assert!(readme.contains("tagged by variant name")); -} - -#[test] -fn readme_documents_optionals() { - let readme = readme_for(&empty_api(), "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("### Optionals")); - assert!(readme.contains("`null`")); - assert!(readme.contains("presence flag")); - assert!(readme.contains("nullable object pointer")); -} - -#[test] -fn readme_documents_lazy_iterators() { - let readme = readme_for(&empty_api(), "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("### Iterators")); - assert!(readme.contains("lazy JS iterator")); - assert!(readme.contains("`return()`")); - assert!(readme.contains("destroyed")); -} - -#[test] -fn readme_documents_lists_and_maps() { - let readme = readme_for(&empty_api(), "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("### Lists and Maps")); - assert!(readme.contains("serialized in a value buffer")); - assert!(readme.contains("pointer + length")); -} - -#[test] -fn readme_documents_error_struct_layout_and_payloads() { - let readme = readme_for(&empty_api(), "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("16 bytes on wasm32"), "{readme}"); - assert!(readme.contains("payload_ptr"), "{readme}"); - assert!( - readme.contains("decodes them from the error's value"), - "{readme}" - ); -} - -#[test] -fn js_stub_has_jsdoc() { - let js = js_stub_for( - &empty_api(), - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - assert!(js.contains("@param {string} url")); - assert!(js.contains("@returns {Promise}")); - assert!(js.contains("@example")); -} - -#[test] -fn js_stub_documents_complex_types() { - let js = js_stub_for( - &empty_api(), - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - assert!(js.contains("Record: plain objects in and out (serialized automatically).")); - assert!(js.contains("Enum: pass the integer discriminant.")); - assert!(js.contains("Optional: pass null to omit, a value to provide.")); - assert!(js.contains("List/Map: pass arrays/objects; receive arrays/objects.")); -} - -#[test] -fn js_stub_has_type_convention_header() { - let js = js_stub_for( - &empty_api(), - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - assert!(js.contains("Objects -> i32 pointer into linear memory (0 = null/absent)")); - assert!(js.contains("Enums -> i32 discriminant value")); - assert!(js.contains("Bytes -> i32 data pointer + i32 length (out_len for returns)")); - assert!(js.contains("Buffered -> records, rich enums, optionals, lists, and maps cross")); - assert!(js.contains("as one value buffer: i32 pointer + i32 length")); -} - -#[test] -fn generate_writes_both_files() { - let tmp = std::env::temp_dir().join("weaveffi_test_wasm_gen"); - let _ = std::fs::remove_dir_all(&tmp); - let out = Utf8Path::from_path(tmp.as_path()).unwrap(); - let api = make_api(vec![]); - WasmGenerator - .generate(&api, out, &WasmConfig::default()) - .unwrap(); - - let readme = std::fs::read_to_string(out.join("wasm/README.md")).unwrap(); - assert!(readme.contains("## Complex Type Handling")); - - let js = std::fs::read_to_string(out.join("wasm/weaveffi_wasm.js")).unwrap(); - assert!(js.contains("export async function loadWeaveffiWasm")); - assert!(js.contains("@param {string} url")); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn empty_api_has_no_api_reference() { - let readme = readme_for(&empty_api(), "weaveffi", "weaveffi.yml", false); - assert!(!readme.contains("## API Reference")); -} - -#[test] -fn api_reference_lists_module() { - let readme = readme_for(&sample_api(), "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("## API Reference")); - assert!(readme.contains("### Module: `math`")); -} - -#[test] -fn api_reference_function_abi_name() { - let readme = readme_for(&sample_api(), "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("##### `weaveffi_math_add`")); -} - -#[test] -fn api_reference_function_signature() { - let readme = readme_for(&sample_api(), "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("`weaveffi_math_add(a: i32, b: i32) -> i32`")); -} - -#[test] -fn api_reference_function_param_table() { - let readme = readme_for(&sample_api(), "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("| `a` | `i32` | `i32` | native Wasm i32 |")); - assert!(readme.contains("| `b` | `i32` | `i32` | native Wasm i32 |")); - assert!(readme.contains("| _returns_ | `i32` | `i32` | native Wasm i32 |")); -} - -#[test] -fn api_reference_function_doc() { - let readme = readme_for(&sample_api(), "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("Add two numbers")); -} - -#[test] -fn api_reference_struct_fields() { - let readme = readme_for(&sample_api(), "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("##### `Point`")); - assert!(readme.contains("A plain JS object, serialized in a value buffer")); - assert!(readme.contains("| `x` | `f64` |")); - assert!(readme.contains("| `y` | `f64` |")); - // Records declare no C symbols: no getters, no create, no destroy. - assert!(!readme.contains("weaveffi_math_Point"), "{readme}"); -} - -#[test] -fn api_reference_enum_discriminants() { - let readme = readme_for(&sample_api(), "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("##### `Color`")); - assert!(readme.contains("`i32` discriminant")); - assert!(readme.contains("| `Red` | `0` |")); - assert!(readme.contains("| `Green` | `1` |")); - assert!(readme.contains("| `Blue` | `2` |")); -} - -#[test] -fn wasm_type_maps_all_variants() { - assert_eq!(wasm_type(&TypeRef::I32), "i32"); - assert_eq!(wasm_type(&TypeRef::U32), "i32"); - assert_eq!(wasm_type(&TypeRef::I64), "i64"); - assert_eq!(wasm_type(&TypeRef::F64), "f64"); - assert_eq!(wasm_type(&TypeRef::Bool), "i32"); - // A string is a single NUL-terminated C string pointer. - assert_eq!(wasm_type(&TypeRef::StringUtf8), "i32"); - assert_eq!(wasm_type(&TypeRef::Bytes), "i32, i32"); - assert_eq!(wasm_type(&TypeRef::Handle), "i64"); - // Buffered value types cross as one ptr + len pair. - assert_eq!(wasm_type(&TypeRef::Record("Foo".into())), "i32, i32"); - assert_eq!(wasm_type(&TypeRef::RichEnum("Shape".into())), "i32, i32"); - assert_eq!(wasm_type(&TypeRef::Enum("Bar".into())), "i32"); - assert_eq!( - wasm_type(&TypeRef::List(Box::new(TypeRef::I32))), - "i32, i32" - ); - assert_eq!( - wasm_type(&TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32) - )), - "i32, i32" - ); - assert_eq!( - wasm_type(&TypeRef::Optional(Box::new(TypeRef::Record("Foo".into())))), - "i32, i32" - ); - assert_eq!( - wasm_type(&TypeRef::Optional(Box::new(TypeRef::I32))), - "i32, i32" - ); - // The one non-buffered optional: a nullable interface pointer. - assert_eq!( - wasm_type(&TypeRef::Optional(Box::new(TypeRef::Interface("S".into())))), - "i32" - ); - assert_eq!(wasm_type(&TypeRef::TypedHandle("Contact".into())), "i32"); - assert_eq!(wasm_type(&TypeRef::Interface("Store".into())), "i32"); -} - -#[test] -fn wasm_type_note_covers_all_variants() { - assert_eq!(wasm_type_note(&TypeRef::I32), "native Wasm i32"); - assert_eq!(wasm_type_note(&TypeRef::U32), "unsigned mapped to i32"); - assert_eq!(wasm_type_note(&TypeRef::Bool), "0 = false, 1 = true"); - assert_eq!( - wasm_type_note(&TypeRef::StringUtf8), - "NUL-terminated C string pointer" - ); - assert_eq!( - wasm_type_note(&TypeRef::Record("X".into())), - "value buffer: ptr + len in linear memory" - ); - assert_eq!( - wasm_type_note(&TypeRef::RichEnum("X".into())), - "value buffer: ptr + len in linear memory" - ); - assert_eq!( - wasm_type_note(&TypeRef::Enum("E".into())), - "variant discriminant" - ); - assert_eq!( - wasm_type_note(&TypeRef::Optional(Box::new(TypeRef::Record("S".into())))), - "value buffer: ptr + len in linear memory" - ); - assert_eq!( - wasm_type_note(&TypeRef::Optional(Box::new(TypeRef::I32))), - "value buffer: ptr + len in linear memory" - ); - assert_eq!( - wasm_type_note(&TypeRef::Optional(Box::new(TypeRef::Interface("S".into())))), - "nullable object pointer, 0 = absent" - ); -} - -#[test] -fn type_display_round_trips() { - assert_eq!(type_display(&TypeRef::I32), "i32"); - assert_eq!(type_display(&TypeRef::StringUtf8), "string"); - assert_eq!(type_display(&TypeRef::Record("Foo".into())), "Foo"); - assert_eq!(type_display(&TypeRef::RichEnum("Shape".into())), "Shape"); - assert_eq!( - type_display(&TypeRef::Optional(Box::new(TypeRef::I32))), - "i32?" - ); - assert_eq!( - type_display(&TypeRef::List(Box::new(TypeRef::StringUtf8))), - "[string]" - ); - assert_eq!( - type_display(&TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32) - )), - "{string:i32}" - ); -} - -/// A `contacts` module with a string-to-optional-record lookup, reused by -/// the API-reference and marshalling tests. -fn contacts_api() -> ResolvedApi { - make_api(vec![Module { - functions: vec![member( - "find", - vec![str_param("name")], - Some(TypeRef::Optional(Box::new(TypeRef::Record( - "Contact".into(), - )))), - false, - false, - )], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![ - field("id", TypeRef::I32), - field("name", TypeRef::StringUtf8), - ], - }], - ..module("contacts") - }]) -} - -#[test] -fn api_reference_complex_types() { - let readme = readme_for(&contacts_api(), "weaveffi", "weaveffi.yml", false); - assert!( - readme.contains("| `name` | `string` | `i32` | NUL-terminated C string pointer |"), - "{readme}" - ); - assert!( - readme.contains( - "| _returns_ | `Contact?` | `i32, i32` | value buffer: ptr + len in linear memory |" - ), - "{readme}" - ); - assert!( - !readme.contains("weaveffi_contacts_Contact_get"), - "{readme}" - ); -} - -#[test] -fn api_reference_void_return() { - let api = make_api(vec![Module { - functions: vec![member("print", vec![str_param("msg")], None, false, false)], - ..module("io") - }]); - let readme = readme_for(&api, "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("-> void`")); - assert!(!readme.contains("_returns_")); -} - -#[test] -fn api_reference_multiple_modules() { - let api = make_api(vec![module("math"), module("io")]); - let readme = readme_for(&api, "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("### Module: `math`")); - assert!(readme.contains("### Module: `io`")); -} - -#[test] -fn generate_writes_api_reference() { - let tmp = std::env::temp_dir().join("weaveffi_test_wasm_gen_api"); - let _ = std::fs::remove_dir_all(&tmp); - let out = Utf8Path::from_path(tmp.as_path()).unwrap(); - let api = sample_api(); - WasmGenerator - .generate(&api, out, &WasmConfig::default()) - .unwrap(); - - let readme = std::fs::read_to_string(out.join("wasm/README.md")).unwrap(); - assert!(readme.contains("## API Reference")); - assert!(readme.contains("weaveffi_math_add")); - assert!(readme.contains("##### `Point`")); - assert!(readme.contains("##### `Color`")); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn wasm_js_has_api_functions() { - let api = sample_api(); - let js = js_stub_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - assert!(js.contains("add(a, b)")); - assert!(js.contains("wasm.weaveffi_math_add(a, b, _err)")); - // The record is a plain value shape: codecs at module scope, no - // wrapper class, no getters, no module-object factory. - assert!(js.contains("function _write_math_Point(w, v) {"), "{js}"); - assert!(js.contains("function _read_math_Point(r) {"), "{js}"); - assert!(js.contains("w.f64(v.x);"), "{js}"); - assert!(js.contains("v.x = r.f64();"), "{js}"); - assert!(!js.contains("class Point"), "{js}"); - assert!(!js.contains("get x()"), "{js}"); - assert!(!js.contains("Point: {"), "{js}"); - assert!(js.contains("export const Color = Object.freeze(")); - assert!(js.contains("Red: 0")); - assert!(js.contains("Green: 1")); - assert!(js.contains("Blue: 2")); - assert!(js.contains("_raw: wasm")); - assert!(js.contains("math: {")); -} - -#[test] -fn wasm_js_emits_buffer_runtime_when_records_present() { - let js = js_stub_for( - &sample_api(), - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - assert!(js.contains("class _BufWriter {"), "{js}"); - assert!(js.contains("class _BufReader {"), "{js}"); - // The reader is strict: little-endian DataView reads plus validation. - assert!(js.contains("truncated"), "{js}"); - assert!(js.contains("bool byte out of range"), "{js}"); - assert!(js.contains("option flag byte out of range"), "{js}"); - assert!( - js.contains("length prefix exceeds remaining buffer"), - "{js}" - ); - assert!(js.contains("trailing bytes after value"), "{js}"); - assert!(js.contains("string is not valid UTF-8"), "{js}"); - assert!(js.contains("{ fatal: true }"), "{js}"); -} - -#[test] -fn wasm_generates_dts() { - let tmp = std::env::temp_dir().join("weaveffi_test_wasm_dts"); - let _ = std::fs::remove_dir_all(&tmp); - let out = Utf8Path::from_path(tmp.as_path()).unwrap(); - let api = sample_api(); - WasmGenerator - .generate(&api, out, &WasmConfig::default()) - .unwrap(); - - let dts = std::fs::read_to_string(out.join("wasm/weaveffi_wasm.d.ts")).unwrap(); - assert!(dts.contains("export interface WeaveffiWasmModule")); - assert!( - dts.contains("export function loadWeaveffiWasm(url: string): Promise") - ); - assert!(dts.contains("add(a: number, b: number): number")); - // Records are plain object interfaces: mutable fields, no free(). - assert!(dts.contains("export interface Point")); - assert!(dts.contains(" x: number;")); - assert!(dts.contains(" y: number;")); - assert!(!dts.contains("readonly x")); - assert!(!dts.contains("free(): void;")); - assert!(dts.contains("export declare const Color")); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn wasm_js_has_string_helpers() { - let api = make_api(vec![Module { - functions: vec![member( - "greet", - vec![str_param("name")], - Some(TypeRef::StringUtf8), - false, - false, - )], - ..module("greeting") - }]); - let js = js_stub_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - assert!(js.contains("function _cstr(wasm, str)")); - assert!(js.contains("function _readCStr(wasm, ptr)")); - assert!(js.contains("function _takeCStr(wasm, ptr)")); - assert!(js.contains("TextEncoder")); - assert!(js.contains("TextDecoder")); - assert!(js.contains("_cstr(wasm, name)")); - assert!(js.contains("_takeCStr(wasm,")); - assert!(js.contains("greet(name)")); - assert!(js.contains("wasm.weaveffi_greeting_greet(")); -} - -#[test] -fn wasm_js_has_error_helpers() { - let api = sample_api(); - let js = js_stub_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - assert!(js.contains("function _allocErr(wasm)")); - assert!(js.contains("function _checkErr(wasm, errPtr)")); - // The slot is the 16-byte error struct with payload fields. - assert!(js.contains("wasm.weaveffi_alloc(16)")); - assert!(js.contains("wasm.weaveffi_dealloc(errPtr, 16);")); -} - -#[test] -fn wasm_js_function_passes_err() { - let api = sample_api(); - let js = js_stub_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - assert!(js.contains("const _err = _allocErr(wasm)")); - assert!(js.contains("_checkErr(wasm, _err)")); -} - -#[test] -fn wasm_dts_has_throws_doc() { - let api = sample_api(); - let dts = dts_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi.yml", - "weaveffi_wasm.d.ts", - false, - ); - assert!( - dts.contains("@throws"), - "Expected .d.ts to contain @throws JSDoc comment" - ); - assert!(dts.contains("@throws {WeaveFFIError} if the native call fails")); -} - -#[test] -fn wasm_custom_module_name() { - let tmp = std::env::temp_dir().join("weaveffi_test_wasm_custom_name"); - let _ = std::fs::remove_dir_all(&tmp); - let out = Utf8Path::from_path(tmp.as_path()).unwrap(); - let api = sample_api(); - let config = WasmConfig { - module_name: Some("my_bindings".into()), - ..WasmConfig::default() - }; - WasmGenerator.generate(&api, out, &config).unwrap(); - - assert!(out.join("wasm/my_bindings.js").exists()); - assert!(out.join("wasm/my_bindings.d.ts").exists()); - - let js = std::fs::read_to_string(out.join("wasm/my_bindings.js")).unwrap(); - assert!(js.contains("loadMyBindings")); - - let dts = std::fs::read_to_string(out.join("wasm/my_bindings.d.ts")).unwrap(); - assert!(dts.contains("MyBindingsModule")); - assert!(dts.contains("loadMyBindings")); - - let files = WasmGenerator.output_files(&api, out, &config); - assert!(files.iter().any(|f| f.contains("my_bindings.js"))); - assert!(files.iter().any(|f| f.contains("my_bindings.d.ts"))); - - let _ = std::fs::remove_dir_all(&tmp); -} - -#[test] -fn wasm_typed_handle_type() { - let api = make_api(vec![Module { - functions: vec![member( - "get_info", - vec![param("contact", TypeRef::TypedHandle("Contact".into()))], - None, - false, - false, - )], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - ..module("contacts") - }]); - let dts = dts_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi.yml", - "weaveffi_wasm.d.ts", - false, - ); - assert!( - dts.contains("contact: number"), - "TypedHandle is an opaque i32 pointer surfaced as number: {dts}" - ); - let js = js_stub_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - assert!( - js.contains("wasm.weaveffi_contacts_get_info(contact, _err)"), - "TypedHandle passes through unwrapped: {js}" - ); - assert!(!js.contains("contact._handle"), "{js}"); -} - -#[test] -fn wasm_deeply_nested_optional() { - let api = make_api(vec![Module { - functions: vec![member( - "process", - vec![param( - "data", - TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional( - Box::new(TypeRef::Record("Contact".into())), - ))))), - )], - None, - false, - false, - )], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - ..module("edge") - }]); - let dts = dts_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi.yml", - "weaveffi_wasm.d.ts", - false, - ); - assert!( - dts.contains("(Contact | null)[] | null"), - "should contain deeply nested optional type: {dts}" - ); -} - -#[test] -fn wasm_map_of_lists() { - let api = make_api(vec![Module { - functions: vec![member( - "process", - vec![param( - "scores", - TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::List(Box::new(TypeRef::I32))), - ), - )], - None, - false, - false, - )], - ..module("edge") - }]); - let dts = dts_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi.yml", - "weaveffi_wasm.d.ts", - false, - ); - assert!( - dts.contains("Record"), - "should contain map of lists type: {dts}" - ); -} - -#[test] -fn wasm_enum_keyed_map() { - let api = make_api(vec![Module { - functions: vec![member( - "process", - vec![param( - "contacts", - TypeRef::Map( - Box::new(TypeRef::Enum("Color".into())), - Box::new(TypeRef::Record("Contact".into())), - ), - )], - None, - false, - false, - )], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - enums: vec![EnumDef { - name: "Color".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }], - ..module("edge") - }]); - let dts = dts_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi.yml", - "weaveffi_wasm.d.ts", - false, - ); - assert!( - dts.contains("Record"), - "should contain enum-keyed map type: {dts}" - ); -} - -/// A one-function API returning a record, exercising both the buffered -/// parameter staging and the buffered return decode. -fn record_roundtrip_api() -> ResolvedApi { - make_api(vec![Module { - functions: vec![member( - "save", - vec![param("contact", TypeRef::Record("Contact".into()))], - Some(TypeRef::Record("Contact".into())), - false, - false, - )], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("name", TypeRef::StringUtf8)], - }], - ..module("contacts") - }]) -} - -#[test] -fn buffered_param_staged_like_bytes_and_deallocated() { - let js = js_for_api(&record_roundtrip_api()); - // Encode into a _BufWriter, stage via _bytes (weaveffi_alloc + copy), - // pass (ptr, len), dealloc after the call. - assert!(js.contains("const a0_w = new _BufWriter();"), "{js}"); - assert!( - js.contains("_write_contacts_Contact(a0_w, contact);"), - "{js}" - ); - assert!( - js.contains("const [a0_p, a0_l] = _bytes(wasm, a0_w.finish());"), - "{js}" - ); - assert!( - js.contains("wasm.weaveffi_contacts_save(a0_p, a0_l, _lp, _err);"), - "{js}" - ); - let call = js.find("wasm.weaveffi_contacts_save(").unwrap(); - let dealloc = js.find("wasm.weaveffi_dealloc(a0_p, a0_l);").unwrap(); - assert!(call < dealloc, "staged encoding freed after the call: {js}"); -} - -#[test] -fn buffered_return_read_decoded_and_freed() { - let js = js_for_api(&record_roundtrip_api()); - // The trailing out_len slot is allocated before the call and read - // (then released) afterwards. - assert!(js.contains("const _lp = wasm.weaveffi_alloc(4);"), "{js}"); - assert!( - js.contains("const _len = new DataView(wasm.memory.buffer).getUint32(_lp, true);"), - "{js}" - ); - assert!(js.contains("wasm.weaveffi_dealloc(_lp, 4);"), "{js}"); - // The buffer is copied out and freed by _takeBytes, then decoded - // strictly (end() rejects trailing bytes). - assert!( - js.contains("const _rd = new _BufReader(_takeBytes(wasm, _r, _len));"), - "{js}" - ); - assert!( - js.contains("const _out = _read_contacts_Contact(_rd);"), - "{js}" - ); - assert!(js.contains("_rd.end();"), "{js}"); - assert!(js.contains("function _takeBytes(wasm, ptr, len)"), "{js}"); - assert!(js.contains("wasm.weaveffi_free_bytes(ptr, len);"), "{js}"); - // Errors are checked before the result is decoded, and no wrapper - // class exists for the record. - let check = js.find("_checkErr(wasm, _err)").expect("error check"); - let decode = js - .find("const _out = _read_contacts_Contact(_rd);") - .expect("record decode"); - assert!(check < decode, "errors checked before decoding: {js}"); - assert!(!js.contains("class Contact"), "{js}"); -} - -#[test] -fn optional_record_return_uses_presence_flag() { - let js = js_for_api(&contacts_api()); - // An optional record is buffered: a one-byte flag, then the value. - assert!( - js.contains("const _out = (_rd.flag() ? _read_contacts_Contact(_rd) : null);"), - "{js}" - ); -} - -#[test] -fn optional_scalar_return_decodes_from_buffer() { - let js = js_for_api(&returning_api( - TypeRef::Optional(Box::new(TypeRef::I32)), - false, - )); - assert!( - js.contains("const _out = (_rd.flag() ? _rd.i32() : null);"), - "{js}" - ); - // The old boxed-scalar protocol is gone. - assert!(!js.contains("wasm.weaveffi_free_bytes(_r, 4);"), "{js}"); -} - -#[test] -fn list_return_decodes_from_buffer() { - let js = js_for_api(&returning_api( - TypeRef::List(Box::new(TypeRef::StringUtf8)), - false, - )); - assert!( - js.contains( - "const _out = (() => { const _n = _rd.len(); const _arr = []; for (let _i = 0; _i < _n; _i++) _arr.push(_rd.str()); return _arr; })();" - ), - "{js}" - ); - // No parallel-array protocol remains. - assert!(!js.contains("_takeStrArray"), "{js}"); -} - -#[test] -fn map_return_decodes_from_buffer() { - let js = js_for_api(&returning_api( - TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - false, - )); - assert!( - js.contains( - "const _out = (() => { const _n = _rd.len(); const _obj = {}; for (let _i = 0; _i < _n; _i++) { const _k = _rd.str(); _obj[_k] = _rd.i32(); } return _obj; })();" - ), - "{js}" - ); - assert!(!js.contains("_ka"), "no parallel key array remains: {js}"); -} - -#[test] -fn list_param_serializes_elements_in_order() { - let api = make_api(vec![Module { - functions: vec![member( - "sum", - vec![param("xs", TypeRef::List(Box::new(TypeRef::I32)))], - Some(TypeRef::I64), - false, - false, - )], - ..module("m") - }]); - let js = js_for_api(&api); - assert!(js.contains("const _a1 = xs || [];"), "{js}"); - assert!(js.contains("a0_w.len(_a1.length);"), "{js}"); - assert!(js.contains("for (const _e1 of _a1) {"), "{js}"); - assert!(js.contains("a0_w.i32(_e1);"), "{js}"); -} - -#[test] -fn map_param_accepts_map_instances_and_plain_objects() { - let api = make_api(vec![Module { - functions: vec![member( - "load", - vec![param( - "scores", - TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)), - )], - None, - false, - false, - )], - ..module("m") - }]); - let js = js_for_api(&api); - assert!( - js.contains("const _m1 = _s1 instanceof Map ? [..._s1.entries()] : Object.entries(_s1);"), - "{js}" - ); - assert!(js.contains("a0_w.len(_m1.length);"), "{js}"); - assert!(js.contains("for (const [_k1, _v1] of _m1) {"), "{js}"); - assert!(js.contains("a0_w.str(_k1);"), "{js}"); - assert!(js.contains("a0_w.i32(_v1);"), "{js}"); -} - -#[test] -fn optional_param_writes_presence_flag() { - let api = make_api(vec![Module { - functions: vec![member( - "set_timeout", - vec![param("ms", TypeRef::Optional(Box::new(TypeRef::I32)))], - None, - false, - false, - )], - ..module("m") - }]); - let js = js_for_api(&api); - assert!( - js.contains("if (ms === null || ms === undefined) {"), - "{js}" - ); - assert!(js.contains("a0_w.flag(false);"), "{js}"); - assert!(js.contains("a0_w.flag(true);"), "{js}"); - assert!(js.contains("a0_w.i32(ms);"), "{js}"); -} - -#[test] -fn wasm_async_returns_promise() { - let api = make_api(vec![Module { - functions: vec![Function { - name: "compute".into(), - params: vec![param("x", TypeRef::I32)], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - ..module("math") - }]); - let js = js_stub_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - assert!( - js.contains("new Promise"), - "async function should return a Promise: {js}" - ); - assert!( - js.contains("resolve"), - "Promise should have resolve callback: {js}" - ); - assert!( - js.contains("reject"), - "Promise should have reject callback: {js}" - ); - assert!( - js.contains("_asyncContexts"), - "should use async context map: {js}" - ); - assert!( - js.contains("_registerTrampoline"), - "should register trampoline in function table: {js}" - ); - assert!( - js.contains("weaveffi_math_compute_async("), - "should call the _async export: {js}" - ); - assert!( - js.contains("__indirect_function_table"), - "should reference the Wasm function table: {js}" - ); -} - -/// The Wasm bindings register one trampoline per async-callback -/// signature on the indirect function table for the lifetime of the API -/// instance and route per-call resolve/reject through the -/// `_asyncContexts` map. Each entry is `set(ctxId, ...)` once and -/// `delete(ctxId)` once on the callback path so the resolver closures do -/// not leak. -#[test] -fn wasm_async_pins_callback_for_lifetime() { - let api = make_api(vec![Module { - functions: vec![Function { - name: "compute".into(), - params: vec![param("x", TypeRef::I32)], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - ..module("math") - }]); - let js = js_stub_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - let trampoline_count = js.matches("_registerTrampoline").count(); - let set_count = js.matches("_asyncContexts.set(ctxId").count(); - let delete_count = js.matches("_asyncContexts.delete(ctxId)").count(); - // Trampoline is defined once and registered once per signature. - assert_eq!( - trampoline_count, 2, - "expected one definition and one registration of the trampoline, got {trampoline_count}: {js}" - ); - assert_eq!( - set_count, delete_count, - "every _asyncContexts.set must be matched by a delete: set={set_count} delete={delete_count}: {js}" - ); - assert!( - set_count >= 1, - "expected at least one _asyncContexts.set per async fn: {js}" - ); -} - -#[test] -fn wasm_dts_async_function() { - let api = make_api(vec![Module { - functions: vec![ - Function { - name: "compute".into(), - params: vec![param("x", TypeRef::I32)], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }, - member( - "add", - vec![param("a", TypeRef::I32), param("b", TypeRef::I32)], - Some(TypeRef::I32), - false, - false, - ), - ], - ..module("math") - }]); - let dts = dts_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi.yml", - "weaveffi_wasm.d.ts", - false, - ); - assert!( - dts.contains("compute(x: number): Promise"), - "async function should return Promise in .d.ts: {dts}" - ); - assert!( - dts.contains("add(a: number, b: number): number"), - "sync function should not return Promise: {dts}" - ); - assert!( - !dts.contains("add(a: number, b: number): Promise"), - "sync function must not return Promise: {dts}" - ); -} - -#[test] -fn wasm_nested_module_output() { - let api = make_api(vec![Module { - functions: vec![member("outer_fn", vec![], Some(TypeRef::I32), false, false)], - modules: vec![Module { - functions: vec![member("inner_fn", vec![], Some(TypeRef::I32), false, false)], - ..module("child") - }], - ..module("parent") - }]); - let dts = dts_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi.yml", - "weaveffi_wasm.d.ts", - false, - ); - assert!( - dts.contains("parent:"), - "parent module in DTS interface missing: {dts}" - ); - assert!( - dts.contains("child:"), - "nested child module in DTS interface missing: {dts}" - ); - assert!( - dts.contains("outerFn(): number"), - "parent function in DTS missing: {dts}" - ); - assert!( - dts.contains("innerFn(): number"), - "nested child function in DTS missing: {dts}" - ); - let js = js_stub_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - assert!( - js.contains("weaveffi_parent_outer_fn"), - "parent C ABI call in JS missing: {js}" - ); - assert!( - js.contains("weaveffi_parent_child_inner_fn"), - "nested child C ABI call in JS missing: {js}" - ); -} - -fn doc_module() -> Module { - Module { - functions: vec![Function { - name: "do_thing".into(), - params: vec![Param { - name: "x".into(), - ty: TypeRef::I32, - mutable: false, - doc: Some("the input value".into()), - }], - returns: Some(TypeRef::I32), - doc: Some("Performs a thing.".into()), - throws: false, - r#async: false, - cancellable: false, - deprecated: None, - since: None, - }], - structs: vec![StructDef { - name: "Item".into(), - doc: Some("An item we track.".into()), - fields: vec![StructField { - name: "id".into(), - ty: TypeRef::I64, - doc: Some("Stable id".into()), - }], - }], - enums: vec![EnumDef { - name: "Kind".into(), - doc: Some("Kind of item.".into()), - variants: vec![EnumVariant { - name: "Small".into(), - value: 0, - doc: Some("A small one".into()), - fields: vec![], - }], - }], - ..module("docs") - } -} - -#[test] -fn wasm_emits_doc_on_function() { - let dts = dts_for( - &make_api(vec![doc_module()]), - "weaveffi", - "weaveffi.yml", - "weaveffi.d.ts", - false, - ); - assert!(dts.contains("Performs a thing."), "{dts}"); -} - -#[test] -fn wasm_emits_doc_on_struct() { - let dts = dts_for( - &make_api(vec![doc_module()]), - "weaveffi", - "weaveffi.yml", - "weaveffi.d.ts", - false, - ); - assert!(dts.contains("/** An item we track. */"), "{dts}"); -} - -#[test] -fn wasm_emits_doc_on_enum_variant() { - let dts = dts_for( - &make_api(vec![doc_module()]), - "weaveffi", - "weaveffi.yml", - "weaveffi.d.ts", - false, - ); - assert!(dts.contains("/** Kind of item. */"), "{dts}"); - assert!(dts.contains("/** A small one */"), "{dts}"); -} - -#[test] -fn wasm_emits_doc_on_field() { - let dts = dts_for( - &make_api(vec![doc_module()]), - "weaveffi", - "weaveffi.yml", - "weaveffi.d.ts", - false, - ); - assert!(dts.contains("/** Stable id */"), "{dts}"); -} - -#[test] -fn wasm_emits_doc_on_param() { - let dts = dts_for( - &make_api(vec![doc_module()]), - "weaveffi", - "weaveffi.yml", - "weaveffi.d.ts", - false, - ); - assert!(dts.contains("@param x the input value"), "{dts}"); -} - -#[test] -fn wasm_custom_prefix_threads_to_user_symbols() { - let js = js_stub_for( - &sample_api(), - DEFAULT_MODULE_NAME, - "myffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - // User-exported symbols honor the configured C ABI prefix. - assert!( - js.contains("myffi_math_add"), - "user export should use the custom prefix: {js}" - ); - assert!( - !js.contains("weaveffi_math_add"), - "user export must not hard-code the weaveffi_ prefix: {js}" - ); - // Runtime ABI helpers exported by weaveffi-abi stay literal. - assert!( - js.contains("weaveffi_alloc"), - "runtime alloc helper must stay literal: {js}" - ); - assert!( - js.contains("weaveffi_error_clear"), - "runtime error_clear helper must stay literal: {js}" - ); -} - -/// A rich (algebraic) enum mirroring `samples/shapes`: a unit variant, an -/// f64 payload, two f32 payloads, and a string + u8 payload, plus a plain -/// sibling enum and free functions taking/returning the rich enum (already -/// resolved to `TypeRef::RichEnum`) so the value-buffer marshalling is -/// exercised too. -fn rich_enum_api() -> ResolvedApi { - make_api(vec![Module { - functions: vec![ - member( - "describe", - vec![param("shape", TypeRef::RichEnum("Shape".into()))], - Some(TypeRef::StringUtf8), - false, - false, - ), - member( - "scale", - vec![ - param("shape", TypeRef::RichEnum("Shape".into())), - param("factor", TypeRef::F64), - ], - Some(TypeRef::RichEnum("Shape".into())), - false, - false, - ), - member( - "sum_bytes", - vec![param("values", TypeRef::List(Box::new(TypeRef::U8)))], - Some(TypeRef::U64), - false, - false, - ), - ], - enums: vec![ - EnumDef { - name: "Shape".into(), - doc: Some("An algebraic shape".into()), - variants: vec![ - EnumVariant { - name: "Empty".into(), - value: 0, - doc: Some("The empty shape".into()), - fields: vec![], - }, - EnumVariant { - name: "Circle".into(), - value: 1, - doc: None, - fields: vec![field("radius", TypeRef::F64)], - }, - EnumVariant { - name: "Rectangle".into(), - value: 2, - doc: None, - fields: vec![field("width", TypeRef::F32), field("height", TypeRef::F32)], - }, - EnumVariant { - name: "Labeled".into(), - value: 3, - doc: None, - fields: vec![ - field("label", TypeRef::StringUtf8), - field("count", TypeRef::U8), - ], - }, - ], - }, - EnumDef { - name: "Channel".into(), - doc: None, - variants: vec![ - EnumVariant { - name: "Red".into(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "Green".into(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }, - ], - ..module("shapes") - }]) -} - -#[test] -fn wasm_rich_enum_emits_buffer_codecs() { - let js = js_stub_for( - &rich_enum_api(), - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - // The writer switches on the string tag and packs the i32 discriminant - // plus the active variant's fields in declaration order. - assert!(js.contains("function _write_shapes_Shape(w, v) {"), "{js}"); - assert!(js.contains("switch (v.tag) {"), "{js}"); - assert!(js.contains("case \"Empty\": {"), "{js}"); - assert!(js.contains("case \"Circle\": {"), "{js}"); - assert!(js.contains("w.i32(1);"), "{js}"); - assert!(js.contains("w.f64(v.radius);"), "{js}"); - assert!(js.contains("w.f32(v.width);"), "{js}"); - assert!(js.contains("w.str(v.label);"), "{js}"); - assert!(js.contains("w.u8(v.count);"), "{js}"); - assert!(js.contains("unknown Shape variant tag"), "{js}"); - // The reader switches on the numeric tag and rebuilds the tagged - // plain object. - assert!(js.contains("function _read_shapes_Shape(r) {"), "{js}"); - assert!(js.contains("const _tag = r.i32();"), "{js}"); - assert!(js.contains("return { tag: \"Empty\" };"), "{js}"); - assert!(js.contains("const v = { tag: \"Circle\" };"), "{js}"); - assert!(js.contains("v.radius = r.f64();"), "{js}"); - assert!(js.contains("unknown Shape tag"), "{js}"); - // No handle-wrapper machinery remains. - assert!(!js.contains("class Shape"), "{js}"); - assert!(!js.contains("Shape.Tag"), "{js}"); - assert!(!js.contains("Shape_destroy"), "{js}"); - assert!(!js.contains("Shape_Circle_new"), "{js}"); -} - -#[test] -fn wasm_rich_enum_not_emitted_as_plain_enum_object() { - let js = js_stub_for( - &rich_enum_api(), - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - // The rich enum must NOT be emitted as a by-value discriminant object. - assert!( - !js.contains("export const Shape = Object.freeze("), - "rich enum must not be a plain enum object: {js}" - ); - // A plain sibling enum is still emitted the by-value way. - assert!( - js.contains("export const Channel = Object.freeze("), - "plain enum should still be a frozen object: {js}" - ); -} - -#[test] -fn wasm_rich_enum_function_marshals_value_buffer() { - let js = js_stub_for( - &rich_enum_api(), - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - // A rich enum crosses the ABI as a value buffer: encoded on the way - // in, staged like bytes, decoded on the way out. - assert!(js.contains("_write_shapes_Shape(a0_w, shape);"), "{js}"); - assert!( - js.contains("wasm.weaveffi_shapes_describe(a0_p, a0_l, _err);"), - "describe must pass the staged (ptr, len) pair: {js}" - ); - assert!( - js.contains("wasm.weaveffi_shapes_scale(a0_p, a0_l, factor, _lp, _err);"), - "scale must pass the pair, the scalar, and the out_len slot: {js}" - ); - assert!( - js.contains("const _out = _read_shapes_Shape(_rd);"), - "scale must decode the returned buffer: {js}" - ); - // Errors are checked before the result is decoded. - let check = js - .find("_checkErr(wasm, _err)") - .expect("scale should check the error slot"); - let decode = js - .find("const _out = _read_shapes_Shape(_rd);") - .expect("scale should decode the result"); - assert!( - check < decode, - "errors must be checked before decoding: {js}" - ); -} - -#[test] -fn wasm_rich_enum_dts_union() { - let dts = dts_for( - &rich_enum_api(), - DEFAULT_MODULE_NAME, - "weaveffi.yml", - "weaveffi_wasm.d.ts", - false, - ); - // A discriminated union of plain object shapes, keyed by `tag`. - assert!(dts.contains("export type Shape ="), "{dts}"); - assert!(dts.contains("| { tag: \"Empty\" }"), "{dts}"); - assert!( - dts.contains("| { tag: \"Circle\"; radius: number }"), - "{dts}" - ); - assert!( - dts.contains("| { tag: \"Rectangle\"; width: number; height: number }"), - "{dts}" - ); - assert!( - dts.contains("| { tag: \"Labeled\"; label: string; count: number };"), - "{dts}" - ); - // Not a class, not a by-value const map. - assert!(!dts.contains("export declare class Shape"), "{dts}"); - assert!(!dts.contains("export declare const Shape"), "{dts}"); - assert!( - dts.contains("scale(shape: Shape, factor: number): Shape"), - "functions should reference the union type: {dts}" - ); -} - -#[test] -fn wasm_rich_enum_readme() { - let readme = readme_for(&rich_enum_api(), "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("##### `Shape`"), "{readme}"); - assert!( - readme.contains("Rich (algebraic) enum"), - "rich enum readme should call it out: {readme}" - ); - assert!( - readme.contains("| Variant | Tag | Fields |"), - "rich enum readme should tabulate variants: {readme}" - ); - assert!( - readme.contains("`radius: f64`"), - "rich enum readme should list field types: {readme}" - ); -} - -/// A one-function async API for the Emscripten stub tests. -fn async_api() -> ResolvedApi { - make_api(vec![Module { - functions: vec![Function { - name: "compute".into(), - params: vec![param("x", TypeRef::I32)], - returns: Some(TypeRef::I32), - doc: None, - throws: false, - r#async: true, - cancellable: false, - deprecated: None, - since: None, - }], - ..module("math") - }]) -} - -#[test] -fn emscripten_loader_accepts_module_and_binds_exports() { - let js = js_stub_for( - &sample_api(), - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - true, - ); - assert!( - js.contains("export async function loadWeaveffiWasm(module) {"), - "loader should accept the Emscripten module: {js}" - ); - assert!( - js.contains("const m = await Promise.resolve(module);"), - "loader should accept the MODULARIZE factory promise too: {js}" - ); - assert!( - !js.contains("fetch(url)") && !js.contains("WebAssembly.instantiate"), - "Emscripten mode must not instantiate the wasm itself: {js}" - ); - // Runtime helpers and business symbols bind from the underscore- - // prefixed Module properties, in quoted bracket notation. - assert!( - js.contains("weaveffi_alloc: m['_weaveffi_alloc'],"), - "missing alloc binding: {js}" - ); - assert!( - js.contains("weaveffi_math_add: m['_weaveffi_math_add'],"), - "missing business symbol binding: {js}" - ); - // Records declare no C symbols, so nothing Point-related is bound. - assert!( - !js.contains("m['_weaveffi_math_Point"), - "records must not bind any symbols: {js}" - ); - assert!( - js.contains("get memory() { return { buffer: m['HEAPU8'].buffer }; },"), - "memory must be a live getter over HEAPU8: {js}" - ); -} - -#[test] -fn emscripten_body_stays_identical_to_standard_mode() { - let standard = js_stub_for( - &sample_api(), - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ); - let emscripten = js_stub_for( - &sample_api(), - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - true, - ); - // The adapter confines the divergence to the loader prologue; every - // call site keeps the same dot access on the bound `wasm` object. - assert!( - emscripten.contains("wasm.weaveffi_math_add(a, b, _err)"), - "call sites must not fork per mode: {emscripten}" - ); - for helper in ["function _cstr(wasm, str)", "function _allocErr(wasm)"] { - let body = |s: &str| { - let start = s.find(helper).unwrap_or_else(|| panic!("missing {helper}")); - s[start..s[start..].find("\n\n").map_or(s.len(), |e| start + e)].to_string() - }; - assert_eq!( - body(&standard), - body(&emscripten), - "shared helpers must be byte-identical between modes" - ); - } -} - -#[test] -fn emscripten_binds_prefixed_runtime_helpers() { - let js = js_stub_for( - &sample_api(), - DEFAULT_MODULE_NAME, - "acme", - "weaveffi.yml", - "weaveffi_wasm.js", - true, - ); - // The glue's hardcoded helper names bind to the producer's prefixed - // exports, matching the runtime declarations in the generated header. - assert!( - js.contains("weaveffi_alloc: m['_acme_alloc'],"), - "alloc must map to the prefixed export: {js}" - ); - assert!( - js.contains("weaveffi_error_clear: m['_acme_error_clear'],"), - "error_clear must map to the prefixed export: {js}" - ); - assert!( - js.contains("weaveffi_free_bytes: m['_acme_free_bytes'],"), - "free_bytes must map to the prefixed export: {js}" - ); -} - -#[test] -fn emscripten_async_functions_become_throwing_stubs() { - let js = js_stub_for( - &async_api(), - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - true, - ); - assert!( - js.contains("async function 'compute' is not supported in Emscripten mode"), - "async stub should throw with a clear message: {js}" - ); - assert!( - !js.contains("_registerTrampoline") && !js.contains("WebAssembly.Function"), - "no trampoline machinery in Emscripten mode: {js}" - ); - assert!( - !js.contains("weaveffi_math_compute_async"), - "the async launcher must not be bound or called: {js}" - ); -} - -#[test] -fn emscripten_dts_loader_signature_and_async_omission() { - let dts = dts_for( - &async_api(), - DEFAULT_MODULE_NAME, - "weaveffi.yml", - "weaveffi_wasm.d.ts", - true, - ); - assert!( - dts.contains( - "export function loadWeaveffiWasm(module: object | Promise): \ - Promise;" - ), - "loader signature should take the Emscripten module: {dts}" - ); - assert!( - !dts.contains("compute("), - "async stubs must be omitted from the d.ts: {dts}" - ); - assert!( - dts.contains("_raw: Record;"), - "_raw is the export-binding object in Emscripten mode: {dts}" - ); -} - -#[test] -fn emscripten_readme_documents_emcc_build() { - let readme = readme_for(&async_api(), "weaveffi", "weaveffi.yml", true); - assert!( - readme.contains("emcc"), - "readme should show an emcc invocation: {readme}" - ); - assert!( - readme.contains("EXPORTED_RUNTIME_METHODS=HEAPU8"), - "readme should list the required runtime method export: {readme}" - ); - assert!( - readme.contains("Async functions are not supported in Emscripten mode"), - "readme should call out the async gap: {readme}" - ); -} - -#[test] -fn dts_bytes_map_to_uint8array() { - assert_eq!(ts_type_for(&TypeRef::Bytes), "Uint8Array"); - assert_eq!(ts_type_for(&TypeRef::BorrowedBytes), "Uint8Array"); -} - -// --- Interfaces, typed errors, throws split, naming --- - -/// A kvstore-shaped module: a `Store` interface (canonical `new` plus an -/// `open` factory, sync/iterator/async methods, one static), a `KvError` -/// domain, and one non-throwing free function. -fn kv_api() -> ResolvedApi { - make_api(vec![Module { - functions: vec![member("flush_all", vec![], None, false, false)], - errors: Some(weaveffi_ir::ir::ErrorDomain { - name: "KvError".into(), - codes: vec![ - weaveffi_ir::ir::ErrorCode { - name: "KeyNotFound".into(), - code: 1001, - message: "key not found".into(), - doc: None, - fields: vec![], - }, - weaveffi_ir::ir::ErrorCode { - name: "StoreFull".into(), - code: 1003, - message: "store is full".into(), - doc: None, - fields: vec![], - }, - ], - }), - interfaces: vec![weaveffi_ir::ir::InterfaceDef { - name: "Store".into(), - doc: Some("A key-value store handle.".into()), - constructors: vec![ - member("new", vec![str_param("path")], None, true, false), - member("open", vec![str_param("path")], None, true, false), - ], - methods: vec![ - member( - "put", - vec![str_param("key"), param("ttl_seconds", TypeRef::I64)], - None, - true, - false, - ), - member( - "get", - vec![str_param("key")], - Some(TypeRef::StringUtf8), - true, - false, - ), - member( - "list_keys", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))), - false, - false, - ), - member("compact", vec![], None, true, true), - ], - statics: vec![member( - "default_capacity", - vec![], - Some(TypeRef::U64), - false, - false, - )], - }], - ..module("kv") - }]) -} - -fn kv_js() -> String { - js_stub_for( - &kv_api(), - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ) -} - -#[test] -fn interface_class_has_ctor_wrap_free_and_members() { - let js = kv_js(); - assert!(js.contains("class Store {"), "{js}"); - // Canonical `new` becomes `constructor`, assigning the owned handle. - assert!(js.contains("constructor(path) {"), "{js}"); - assert!( - js.contains("const _r = wasm.weaveffi_kv_Store_new(a0_p, _err);"), - "{js}" - ); - assert!(js.contains("this._handle = _r;"), "{js}"); - // Internal adoption path used by returns and element decoding. - assert!(js.contains("static _wrap(handle) {"), "{js}"); - assert!( - js.contains("const _o = Object.create(Store.prototype);"), - "{js}" - ); - // Non-canonical constructor is a static factory returning a wrapped - // owned handle via the ordinary return path. - assert!(js.contains("static open(path) {"), "{js}"); - assert!(js.contains("return Store._wrap(_r);"), "{js}"); - // Methods pass the instance handle as the implicit leading argument. - assert!(js.contains("put(key, ttlSeconds) {"), "{js}"); - assert!( - js.contains("wasm.weaveffi_kv_Store_put(this._handle, "), - "{js}" - ); - // Statics are static methods. - assert!(js.contains("static defaultCapacity() {"), "{js}"); - // Disposal: free() releases exactly once. - assert!(js.contains("free() {"), "{js}"); - assert!( - js.contains("wasm.weaveffi_kv_Store_destroy(this._handle);"), - "{js}" - ); - // The class itself is exposed on the module object. - assert!(js.contains("Store: Store,"), "{js}"); -} - -#[test] -fn interface_iterator_member_returns_lazy_iterator_with_self() { - let js = kv_js(); - assert!(js.contains("listKeys() {"), "{js}"); - // The launch call threads the instance handle and the throws-aware - // error slot. - assert!( - js.contains("const _it = wasm.weaveffi_kv_Store_list_keys(this._handle, _err);"), - "{js}" - ); - // The wrapper hands the handle to the lazy iterator instead of - // draining it into an array. - assert!( - js.contains("return new _WeaveFFIIterator(wasm, _it, 4,"), - "{js}" - ); - assert!( - js.contains( - "(it, slot, ep) => wasm.weaveffi_kv_Store_ListKeysIterator_next(it, slot, ep)," - ), - "{js}" - ); - assert!( - js.contains("(it) => wasm.weaveffi_kv_Store_ListKeysIterator_destroy(it),"), - "{js}" - ); - // No eager while-drain remains anywhere in the glue. - assert!(!js.contains("while (wasm."), "{js}"); -} - -#[test] -fn lazy_iterator_class_implements_protocol_and_destroys_once() { - let js = kv_js(); - assert!(js.contains("class _WeaveFFIIterator {"), "{js}"); - // Iterator protocol: next(), return() for early exit, and - // [Symbol.iterator]() making it iterable. - assert!(js.contains(" next() {"), "{js}"); - assert!(js.contains(" return(value) {"), "{js}"); - assert!(js.contains(" [Symbol.iterator]() {"), "{js}"); - // One producer next call per consumer step. - assert!( - js.contains("_has = this._callNext(this._handle, this._slot, _err);"), - "{js}" - ); - // Destroy exactly once: _close() nulls the handle, and every path - // (exhaustion, next error, early return) funnels through it. - assert!(js.contains("if (this._handle === 0) return;"), "{js}"); - assert!(js.contains("this._destroyFn(this._handle);"), "{js}"); - assert_eq!(js.matches("this._close();").count(), 3, "{js}"); - // Abandonment leak is documented at the class site. - assert!(js.contains("leaks the"), "{js}"); -} - -#[test] -fn lazy_iterator_frees_string_elements_per_plan() { - let js = kv_js(); - // Each yielded string element is copied out of wasm memory and then - // freed with the runtime's free_string (via _takeCStr). - assert!( - js.contains("(w, p) => _takeCStr(w, new DataView(w.memory.buffer).getUint32(p, true)));"), - "{js}" - ); -} - -#[test] -fn lazy_iterator_next_errors_follow_error_strategy() { - let js = kv_js(); - // list_keys does not throw, so both launch and next route through the - // generic trap checker. - let list_keys = js - .split("listKeys() {") - .nth(1) - .and_then(|s| s.split("\n }").next()) - .expect("listKeys body"); - assert!(list_keys.contains("_checkErr(wasm, _err);"), "{list_keys}"); - assert!( - list_keys.contains("_checkErr, (w, p) =>"), - "next checker must match the function's error strategy: {list_keys}" - ); -} - -#[test] -fn typed_error_classes_and_factory() { - let js = kv_js(); - assert!( - js.contains("export class WeaveFFIError extends Error {"), - "{js}" - ); - assert!( - js.contains("export class KvError extends WeaveFFIError {}"), - "{js}" - ); - assert!( - js.contains("export class KeyNotFound extends KvError {"), - "{js}" - ); - assert!(js.contains("KeyNotFound.CODE = 1001;"), "{js}"); - assert!(js.contains("KvError.KeyNotFound = KeyNotFound;"), "{js}"); - assert!(js.contains("StoreFull.CODE = 1003;"), "{js}"); - // The factory takes the payload slots (unused here: no code declares - // fields) and maps unknown codes to the generic brand error. - assert!( - js.contains("function _kvErrorFrom(wasm, code, message, payloadPtr, payloadLen) {"), - "{js}" - ); - assert!(js.contains("const _cls = _KV_ERROR_CODES[code];"), "{js}"); - assert!(js.contains("new WeaveFFIError(code, message);"), "{js}"); -} - -#[test] -fn throws_split_selects_typed_or_generic_checker() { - let js = kv_js(); - // Throwing members route the out-err slot through the domain checker, - // which reads all four error-struct fields (code, message, payload). - assert!( - js.contains("function _checkKvError(wasm, errPtr) {"), - "{js}" - ); - assert!(js.contains("_checkKvError(wasm, _err);"), "{js}"); - assert!( - js.contains( - "const _e = _kvErrorFrom(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true));" - ), - "{js}" - ); - // The non-throwing free function keeps the generic checker. - assert!(js.contains("flushAll() {"), "{js}"); - let flush = js - .split("flushAll() {") - .nth(1) - .and_then(|s| s.split('}').next()) - .expect("flushAll body"); - assert!(flush.contains("_checkErr(wasm, _err);"), "{flush}"); - assert!(!flush.contains("_checkKvError"), "{flush}"); -} - -#[test] -fn async_throwing_member_rejects_with_domain_error() { - let js = kv_js(); - // The async context carries the domain factory for typed rejection. - assert!( - js.contains("_asyncContexts.set(ctxId, { resolve, reject, mkErr: _kvErrorFrom });"), - "{js}" - ); - assert!( - js.contains("if (errPtr !== 0) _checkErrRef(wasm, errPtr, ctx.mkErr);"), - "{js}" - ); - // The boxed-error checker hands the payload slots to the factory, then - // releases the box before throwing. - assert!( - js.contains( - "const err = mkErr ? mkErr(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true)) : new WeaveFFIError(code, msg);" - ), - "{js}" - ); - assert!( - js.contains("wasm.weaveffi_error_free(errPtr);"), - "the consumer must free the boxed async error: {js}" - ); - // The launcher passes the cancel slot and callback as usual. - assert!( - js.contains( - "wasm.weaveffi_kv_Store_compact_async(this._handle, 0, _cbPtr_i32_i32, ctxId);" - ), - "{js}" - ); -} - -#[test] -fn naming_lower_camel_functions_and_params() { - let js = kv_js(); - assert!(js.contains("flushAll() {"), "{js}"); - assert!(js.contains("put(key, ttlSeconds) {"), "{js}"); - assert!(!js.contains("ttl_seconds"), "{js}"); - assert!(!js.contains("list_keys() {"), "{js}"); -} - -#[test] -fn kv_dts_declares_errors_interface_and_throws_tags() { - let dts = dts_for( - &kv_api(), - DEFAULT_MODULE_NAME, - "weaveffi.yml", - "weaveffi_wasm.d.ts", - false, - ); - assert!( - dts.contains("export declare class WeaveFFIError extends Error {"), - "{dts}" - ); - assert!( - dts.contains("export declare class KvError extends WeaveFFIError {"), - "{dts}" - ); - assert!( - dts.contains("static readonly KeyNotFound: typeof KeyNotFound;"), - "{dts}" - ); - assert!( - dts.contains("export declare class KeyNotFound extends KvError {"), - "{dts}" - ); - assert!(dts.contains("static readonly CODE: 1001;"), "{dts}"); - assert!(dts.contains("export declare class Store {"), "{dts}"); - assert!(dts.contains("constructor(path: string);"), "{dts}"); - assert!(dts.contains("static open(path: string): Store;"), "{dts}"); - assert!( - dts.contains("put(key: string, ttlSeconds: bigint): void;"), - "{dts}" - ); - assert!( - dts.contains("listKeys(): IterableIterator;"), - "{dts}" - ); - assert!( - dts.contains("@returns A lazy iterator"), - "iterator members should document the streaming contract: {dts}" - ); - assert!(dts.contains("compact(): Promise;"), "{dts}"); - assert!(dts.contains("static defaultCapacity(): bigint;"), "{dts}"); - assert!(dts.contains("free(): void;"), "{dts}"); - assert!(dts.contains("Store: typeof Store;"), "{dts}"); - assert!( - dts.contains("@throws {KvError} on a domain error code"), - "{dts}" - ); - assert!( - dts.contains("@throws {WeaveFFIError} if the native call fails"), - "{dts}" - ); -} - -#[test] -fn kv_readme_documents_error_domain_and_interface() { - let readme = readme_for(&kv_api(), "weaveffi", "weaveffi.yml", false); - assert!(readme.contains("Error Domain: `KvError`"), "{readme}"); - assert!( - readme.contains("| `KeyNotFound` | `1001` | key not found | (none) |"), - "{readme}" - ); - assert!(readme.contains("##### `Store`"), "{readme}"); - assert!(readme.contains("weaveffi_kv_Store_destroy"), "{readme}"); -} - -#[test] -fn emscripten_binds_interface_member_and_destroy_symbols() { - let js = js_stub_for( - &kv_api(), - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - true, - ); - assert!( - js.contains("weaveffi_kv_Store_put: m['_weaveffi_kv_Store_put'],"), - "{js}" - ); - assert!( - js.contains("weaveffi_kv_Store_destroy: m['_weaveffi_kv_Store_destroy'],"), - "{js}" - ); - // The async member is a throwing stub; its launcher is never bound. - assert!( - js.contains("async function 'compact' is not supported in Emscripten mode"), - "{js}" - ); - assert!(!js.contains("weaveffi_kv_Store_compact_async"), "{js}"); - // Iterator surface symbols are bound so the lazy wrapper can call them. - assert!( - js.contains( - "weaveffi_kv_Store_ListKeysIterator_next: m['_weaveffi_kv_Store_ListKeysIterator_next']," - ), - "{js}" - ); -} - -#[test] -fn optional_interface_stays_nullable_pointer() { - let api = make_api(vec![Module { - interfaces: vec![weaveffi_ir::ir::InterfaceDef { - name: "Session".into(), - doc: None, - constructors: vec![member("new", vec![], None, false, false)], - methods: vec![member( - "find", - vec![param( - "other", - TypeRef::Optional(Box::new(TypeRef::Interface("Session".into()))), - )], - Some(TypeRef::Optional(Box::new(TypeRef::Interface( - "Session".into(), - )))), - false, - false, - )], - statics: vec![], - }], - ..module("net") - }]); - let js = js_for_api(&api); - // An optional interface is the one non-buffered optional: a nullable - // borrowed pointer in, a nullable owned pointer out. - assert!(js.contains("(other ? other._handle : 0)"), "{js}"); - assert!( - js.contains("return _r === 0 ? null : Session._wrap(_r);"), - "{js}" - ); - let dts = dts_for( - &api, - DEFAULT_MODULE_NAME, - "weaveffi.yml", - "weaveffi_wasm.d.ts", - false, - ); - assert!( - dts.contains("find(other: Session | null): Session | null;"), - "{dts}" - ); -} - -// --- Structured error payloads --- - -/// An error domain where one code declares payload fields, plus a -/// throwing function so the checker path is generated. -fn payload_api() -> ResolvedApi { - make_api(vec![Module { - functions: vec![member("login", vec![str_param("name")], None, true, false)], - errors: Some(weaveffi_ir::ir::ErrorDomain { - name: "AuthError".into(), - codes: vec![ - weaveffi_ir::ir::ErrorCode { - name: "LockedOut".into(), - code: 1001, - message: "locked out".into(), - doc: None, - fields: vec![ - field("retry_after_secs", TypeRef::I32), - field("user", TypeRef::StringUtf8), - ], - }, - weaveffi_ir::ir::ErrorCode { - name: "Denied".into(), - code: 1002, - message: "denied".into(), - doc: None, - fields: vec![], - }, - ], - }), - ..module("auth") - }]) -} - -#[test] -fn error_payload_fields_decoded_and_attached() { - let js = js_for_api(&payload_api()); - // The factory decodes the borrowed payload buffer per code and - // attaches the fields as properties on the thrown error. - assert!( - js.contains("function _authErrorFrom(wasm, code, message, payloadPtr, payloadLen) {"), - "{js}" - ); - assert!(js.contains("case 1001: {"), "{js}"); - assert!(js.contains("_e.retry_after_secs = _rd.i32();"), "{js}"); - assert!(js.contains("_e.user = _rd.str();"), "{js}"); - assert!(js.contains("_rd.end();"), "{js}"); - // The payload-free code has no decode arm. - assert!(!js.contains("case 1002: {"), "{js}"); - // The checker hands the payload slots (offsets 8 and 12 of the - // 16-byte struct) to the factory before clearing the error. - assert!( - js.contains("function _checkAuthError(wasm, errPtr) {"), - "{js}" - ); - let checker = js - .split("function _checkAuthError(wasm, errPtr) {") - .nth(1) - .and_then(|s| s.split("\n}").next()) - .expect("checker body"); - let decode = checker - .find("const _e = _authErrorFrom(wasm, code, msg, dv.getUint32(errPtr + 8, true), dv.getUint32(errPtr + 12, true));") - .expect("checker decodes payload"); - let clear = checker - .find("wasm.weaveffi_error_clear(errPtr);") - .expect("clear"); - assert!( - decode < clear, - "payload must be decoded before error_clear frees it: {checker}" - ); -} - -#[test] -fn error_payload_fields_declared_in_dts() { - let dts = dts_for( - &payload_api(), - DEFAULT_MODULE_NAME, - "weaveffi.yml", - "weaveffi_wasm.d.ts", - false, - ); - assert!( - dts.contains("export declare class LockedOut extends AuthError {"), - "{dts}" - ); - assert!(dts.contains("readonly retry_after_secs: number;"), "{dts}"); - assert!(dts.contains("readonly user: string;"), "{dts}"); -} - -#[test] -fn error_payload_fields_listed_in_readme() { - let readme = readme_for(&payload_api(), "weaveffi", "weaveffi.yml", false); - assert!( - readme.contains( - "| `LockedOut` | `1001` | locked out | `retry_after_secs: i32`, `user: string` |" - ), - "{readme}" - ); - assert!( - readme.contains("| `Denied` | `1002` | denied | (none) |"), - "{readme}" - ); -} - -// --- Buffered iterators and callbacks --- - -#[test] -fn iterator_buffered_elements_decode_then_free() { - let api = make_api(vec![Module { - functions: vec![member( - "scan", - vec![], - Some(TypeRef::Iterator(Box::new(TypeRef::Record("Entry".into())))), - false, - false, - )], - structs: vec![StructDef { - name: "Entry".into(), - doc: None, - fields: vec![field("id", TypeRef::I32)], - }], - ..module("m") - }]); - let js = js_for_api(&api); - // A buffered element writes through two out slots (ptr at p, len at - // p + 4), so the slot is 8 bytes and next threads both pointers. - assert!( - js.contains("return new _WeaveFFIIterator(wasm, _it, 8,"), - "{js}" - ); - assert!(js.contains("_next(it, slot, slot + 4, ep),"), "{js}"); - // Each element is copied out and freed (via _takeBytes -> - // weaveffi_free_bytes), then strictly decoded. - assert!( - js.contains( - "const _rd = new _BufReader(_takeBytes(w, dv.getUint32(p, true), dv.getUint32(p + 4, true)));" - ), - "{js}" - ); - assert!(js.contains("const _v = _read_m_Entry(_rd);"), "{js}"); - assert!(js.contains("_rd.end(); return _v;"), "{js}"); -} - -#[test] -fn callback_buffered_argument_decoded_borrowed() { - let api = make_api(vec![Module { - structs: vec![StructDef { - name: "Msg".into(), - doc: None, - fields: vec![field("text", TypeRef::StringUtf8)], - }], - callbacks: vec![weaveffi_ir::ir::CallbackDef { - name: "OnMessage".into(), - params: vec![param("msg", TypeRef::Record("Msg".into()))], - doc: None, - }], - listeners: vec![weaveffi_ir::ir::ListenerDef { - name: "message_listener".into(), - event_callback: "OnMessage".into(), - doc: None, - }], - ..module("events") - }]); - let js = js_for_api(&api); - // The buffered argument occupies two i32 slots plus the context slot. - assert!( - js.contains("_registerTrampoline(_table, ['i32', 'i32', 'i32'],"), - "{js}" - ); - // Borrowed: the encoding is copied out of wasm memory (never freed) - // and decoded before the subscriber runs. - assert!( - js.contains( - "const _p0_b = (a0 === 0 || a1 === 0) ? new Uint8Array(0) : new Uint8Array(wasm.memory.buffer, a0, a1).slice();" - ), - "{js}" - ); - assert!(js.contains("const _p0 = _read_events_Msg(_p0_r);"), "{js}"); - assert!(js.contains("_p0_r.end();"), "{js}"); - assert!(js.contains("_l.callback(_p0);"), "{js}"); -} - -// --- Async completion contract: borrowed buffers are copied, not freed --- - -/// A one-module API with a single free function of the given return type. -fn returning_api(ret: TypeRef, is_async: bool) -> ResolvedApi { - make_api(vec![Module { - functions: vec![member("get_it", vec![], Some(ret), false, is_async)], - ..module("m") - }]) -} - -fn js_for_api(api: &ResolvedApi) -> String { - js_stub_for( - api, - DEFAULT_MODULE_NAME, - "weaveffi", - "weaveffi.yml", - "weaveffi_wasm.js", - false, - ) -} - -#[test] -fn async_string_result_is_copied_then_freed() { - let js = js_for_api(&returning_api(TypeRef::StringUtf8, true)); - assert!(js.contains("unwrap: (w, p) => {"), "{js}"); - assert!( - js.contains("const _s = _readCStr(w, p);"), - "async string results are copied out of wasm memory: {js}" - ); - assert!( - js.contains("if (p !== 0) w.weaveffi_free_string(p);"), - "async string results are owned and must be freed: {js}" - ); -} - -#[test] -fn async_bytes_result_is_copied_then_freed() { - let js = js_for_api(&returning_api(TypeRef::Bytes, true)); - assert!( - js.contains("new Uint8Array(w.memory.buffer, ptr, len).slice();"), - "async bytes results must be deep-copied: {js}" - ); - assert!( - js.contains("if (ptr !== 0) w.weaveffi_free_bytes(ptr, len);"), - "async bytes results are owned and must be freed: {js}" - ); -} - -#[test] -fn async_buffered_result_decoded_inside_callback_then_freed() { - let js = js_for_api(&returning_api( - TypeRef::List(Box::new(TypeRef::StringUtf8)), - true, - )); - // The owned value buffer arrives as (ptr, len): the callback copies it - // out of wasm memory, frees the producer allocation, then decodes. - assert!(js.contains("unwrap: (w, ptr, len) => {"), "{js}"); - assert!( - js.contains( - "const _b = ptr === 0 || len === 0 ? new Uint8Array(0) : new Uint8Array(w.memory.buffer, ptr, len).slice();" - ), - "{js}" - ); - assert!( - js.contains("if (ptr !== 0) w.weaveffi_free_bytes(ptr, len);"), - "the consumer frees the owned result buffer: {js}" - ); - assert!(js.contains("_arr.push(_rd.str())"), "{js}"); - assert!(js.contains("_rd.end();"), "{js}"); - // The completion callback carries (ctx, err, ptr, len): four i32s. - assert!(js.contains("_cbPtr_i32_i32_i32_i32"), "{js}"); - assert!( - js.contains("wasm.weaveffi_m_get_it_async(0, _cbPtr_i32_i32_i32_i32, ctxId);"), - "{js}" - ); -} - -#[test] -fn async_optional_scalar_result_decodes_from_buffer() { - let js = js_for_api(&returning_api( - TypeRef::Optional(Box::new(TypeRef::I32)), - true, - )); - assert!( - js.contains("const _v = (_rd.flag() ? _rd.i32() : null);"), - "async optional scalars decode from the borrowed buffer: {js}" - ); -} - -#[test] -fn async_record_result_decoded_from_buffer() { - let api = make_api(vec![Module { - functions: vec![member( - "get_it", - vec![], - Some(TypeRef::Record("Contact".into())), - false, - true, - )], - structs: vec![StructDef { - name: "Contact".into(), - doc: None, - fields: vec![field("id", TypeRef::I32)], - }], - ..module("m") - }]); - let js = js_for_api(&api); - // A record result is a borrowed value buffer, decoded in the callback. - assert!(js.contains("const _v = _read_m_Contact(_rd);"), "{js}"); - assert!(!js.contains("new Contact(w, h)"), "{js}"); -} - -#[test] -fn async_interface_result_is_adopted() { - let api = make_api(vec![Module { - functions: vec![member( - "connect", - vec![], - Some(TypeRef::Interface("Session".into())), - false, - true, - )], - interfaces: vec![weaveffi_ir::ir::InterfaceDef { - name: "Session".into(), - doc: None, - constructors: vec![member("new", vec![], None, false, false)], - methods: vec![], - statics: vec![], - }], - ..module("net") - }]); - let js = js_for_api(&api); - // An owned-object result transfers ownership: the callback adopts the - // pointer into a wrapper whose free() calls the destroy symbol. - assert!( - js.contains("unwrap: (w, h) => Session._wrap(h) });"), - "{js}" - ); -} - -// --- Regression tests for shared-framework behavior fixes --- - -#[test] -fn reserved_word_params_are_escaped() { - // `new` and `delete` are JS reserved words; before the shared - // `lang::escape_ident` adoption the emitted wrapper declared them - // verbatim, producing a syntax error at load time. - let api = make_api(vec![Module { - functions: vec![member( - "configure", - vec![param("new", TypeRef::Bool), param("delete", TypeRef::Bool)], - None, - false, - false, - )], - ..module("m") - }]); - let js = js_for_api(&api); - assert!(js.contains("configure(new_, delete_) {"), "{js}"); - assert!(js.contains("new_ ? 1 : 0"), "{js}"); - assert!(js.contains("delete_ ? 1 : 0"), "{js}"); - let dts = dts_for(&api, DEFAULT_MODULE_NAME, "weaveffi.yml", "w.d.ts", false); - assert!( - dts.contains("configure(new_: boolean, delete_: boolean): void;"), - "{dts}" - ); -} - -#[test] -fn negative_runtime_codes_fall_through_to_brand_error() { - // Domain codes are validated positive-only; the reserved runtime codes - // (-1 generic, -2 panic, -3 marshalling) must never match a typed class. - let js = kv_js(); - // The frozen table maps only the declared positive codes. - assert!( - js.contains("const _KV_ERROR_CODES = Object.freeze({"), - "{js}" - ); - assert!(js.contains("1001: KeyNotFound,"), "{js}"); - assert!(js.contains("1003: StoreFull,"), "{js}"); - assert!( - !js.contains("-1:") && !js.contains("-2:") && !js.contains("-3:"), - "no negative code may appear in a domain table: {js}" - ); - // Throwing path: a table miss (every negative runtime code) builds the - // generic brand error instead of a typed subclass. - assert!( - js.contains( - "const _e = _cls ? (message ? new _cls(message) : new _cls()) : new WeaveFFIError(code, message);" - ), - "{js}" - ); - // Trap path: non-throwing wrappers always surface the brand error. - assert!(js.contains("throw new WeaveFFIError(code, msg);"), "{js}"); -} - -#[test] -fn package_json_escapes_user_strings() { - use crate::package::render_wasm_package_json; - use weaveffi_core::pkg::ResolvedPackage; - - // Before the `JsonObject` adoption these strings were interpolated - // verbatim, so an embedded quote or backslash corrupted the manifest. - let package = ResolvedPackage { - name: "demo".into(), - version: "1.0.0".into(), - description: Some("a \"quoted\" description with a back\\slash".into()), - license: Some("MIT OR \"Custom\"".into()), - authors: vec!["Ada \"the\" Author".into()], - homepage: None, - repository: None, - }; - let json = render_wasm_package_json(&package, "w.js", "w.d.ts", "weaveffi.yml"); - assert!( - json.contains(r#""description": "a \"quoted\" description with a back\\slash""#), - "{json}" - ); - assert!(json.contains(r#""license": "MIT OR \"Custom\"""#), "{json}"); - assert!(json.contains(r#""author": "Ada \"the\" Author""#), "{json}"); -} diff --git a/crates/weaveffi-gen-wasm/src/types.rs b/crates/weaveffi-gen-wasm/src/types.rs index 6c1b4715..bd4839f9 100644 --- a/crates/weaveffi-gen-wasm/src/types.rs +++ b/crates/weaveffi-gen-wasm/src/types.rs @@ -7,140 +7,123 @@ //! deep type scans that decide which runtime helpers the loader embeds. use heck::ToLowerCamelCase; -use weaveffi_core::abi::is_buffered; use weaveffi_core::lang; +use weaveffi_core::model::Ty; use weaveffi_core::model::{ErrorBinding, FnBinding, ParamBinding}; use weaveffi_core::plan::ErrorStrategy; use weaveffi_core::utils::local_type_name; -use weaveffi_ir::ir::{Api, Module, TypeRef}; /// The wasm value-type spelling of one IDL type at the boundary, for the /// README's signature tables. Buffered types occupy two `i32` slots (pointer /// plus length); everything else keeps its scalar or pointer slot. -pub(crate) fn wasm_type(ty: &TypeRef) -> &'static str { - if is_buffered(ty) { +pub(crate) fn wasm_type(ty: &Ty) -> &'static str { + if ty.is_buffered() { return "i32, i32"; } match ty { - TypeRef::I8 - | TypeRef::I16 - | TypeRef::I32 - | TypeRef::U8 - | TypeRef::U16 - | TypeRef::U32 - | TypeRef::Bool - | TypeRef::Enum(_) => "i32", - TypeRef::I64 | TypeRef::U64 | TypeRef::Handle => "i64", - TypeRef::F32 => "f32", - TypeRef::F64 => "f64", - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "i32", - TypeRef::Bytes | TypeRef::BorrowedBytes => "i32, i32", - TypeRef::TypedHandle(_) | TypeRef::Interface(_) | TypeRef::Iterator(_) => "i32", + Ty::I8 | Ty::I16 | Ty::I32 | Ty::U8 | Ty::U16 | Ty::U32 | Ty::Bool | Ty::Enum(_) => "i32", + Ty::I64 | Ty::U64 | Ty::Handle => "i64", + Ty::F32 => "f32", + Ty::F64 => "f64", + Ty::StringUtf8 | Ty::BorrowedStr => "i32", + Ty::Bytes | Ty::BorrowedBytes => "i32, i32", + Ty::TypedHandle(_) | Ty::Interface(_) | Ty::Iterator(_) => "i32", // Only `Interface?` reaches here: a nullable object pointer. - TypeRef::Optional(_) => "i32", - TypeRef::Record(_) | TypeRef::RichEnum(_) | TypeRef::List(_) | TypeRef::Map(_, _) => { + Ty::Optional(_) => "i32", + Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => { unreachable!("buffered type handled above") } - TypeRef::Named(_) => unreachable!("unresolved type reference"), } } /// The one-line boundary note the README's signature tables attach to one /// IDL type: how its wasm slot(s) are interpreted. -pub(crate) fn wasm_type_note(ty: &TypeRef) -> &'static str { - if is_buffered(ty) { +pub(crate) fn wasm_type_note(ty: &Ty) -> &'static str { + if ty.is_buffered() { return "value buffer: ptr + len in linear memory"; } match ty { - TypeRef::I8 => "8-bit signed mapped to i32", - TypeRef::I16 => "16-bit signed mapped to i32", - TypeRef::I32 => "native Wasm i32", - TypeRef::U8 => "8-bit unsigned mapped to i32", - TypeRef::U16 => "16-bit unsigned mapped to i32", - TypeRef::U32 => "unsigned mapped to i32", - TypeRef::I64 => "native Wasm i64", - TypeRef::U64 => "unsigned mapped to i64", - TypeRef::F32 => "native Wasm f32", - TypeRef::F64 => "native Wasm f64", - TypeRef::Bool => "0 = false, 1 = true", - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "NUL-terminated C string pointer", - TypeRef::Bytes | TypeRef::BorrowedBytes => "ptr + len in linear memory", - TypeRef::TypedHandle(_) => "opaque pointer", - TypeRef::Handle => "opaque 64-bit handle", - TypeRef::Interface(_) => "opaque object pointer", - TypeRef::Enum(_) => "variant discriminant", - TypeRef::Iterator(_) => "opaque iterator handle", - TypeRef::Optional(_) => "nullable object pointer, 0 = absent", - TypeRef::Record(_) | TypeRef::RichEnum(_) | TypeRef::List(_) | TypeRef::Map(_, _) => { + Ty::I8 => "8-bit signed mapped to i32", + Ty::I16 => "16-bit signed mapped to i32", + Ty::I32 => "native Wasm i32", + Ty::U8 => "8-bit unsigned mapped to i32", + Ty::U16 => "16-bit unsigned mapped to i32", + Ty::U32 => "unsigned mapped to i32", + Ty::I64 => "native Wasm i64", + Ty::U64 => "unsigned mapped to i64", + Ty::F32 => "native Wasm f32", + Ty::F64 => "native Wasm f64", + Ty::Bool => "0 = false, 1 = true", + Ty::StringUtf8 | Ty::BorrowedStr => "NUL-terminated C string pointer", + Ty::Bytes | Ty::BorrowedBytes => "ptr + len in linear memory", + Ty::TypedHandle(_) => "opaque pointer", + Ty::Handle => "opaque 64-bit handle", + Ty::Interface(_) => "opaque object pointer", + Ty::Enum(_) => "variant discriminant", + Ty::Iterator(_) => "opaque iterator handle", + Ty::Optional(_) => "nullable object pointer, 0 = absent", + Ty::Record(_) | Ty::RichEnum(_) | Ty::List(_) | Ty::Map(_, _) => { unreachable!("buffered type handled above") } - TypeRef::Named(_) => unreachable!("unresolved type reference"), } } /// The IDL-flavored display spelling of one type for the README's API /// reference (`Contact?`, `[string]`, `{string:i32}`). -pub(crate) fn type_display(ty: &TypeRef) -> String { +pub(crate) fn type_display(ty: &Ty) -> String { match ty { - TypeRef::I8 => "i8".into(), - TypeRef::I16 => "i16".into(), - TypeRef::I32 => "i32".into(), - TypeRef::U8 => "u8".into(), - TypeRef::U16 => "u16".into(), - TypeRef::U32 => "u32".into(), - TypeRef::I64 => "i64".into(), - TypeRef::U64 => "u64".into(), - TypeRef::F32 => "f32".into(), - TypeRef::F64 => "f64".into(), - TypeRef::Bool => "bool".into(), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "string".into(), - TypeRef::Bytes | TypeRef::BorrowedBytes => "bytes".into(), - TypeRef::TypedHandle(_) | TypeRef::Handle => "handle".into(), - TypeRef::Record(n) | TypeRef::RichEnum(n) => local_type_name(n).to_string(), - TypeRef::Enum(n) => n.clone(), - TypeRef::Optional(inner) => format!("{}?", type_display(inner)), - TypeRef::List(inner) => format!("[{}]", type_display(inner)), - TypeRef::Iterator(inner) => format!("iter<{}>", type_display(inner)), - TypeRef::Map(k, v) => format!("{{{}:{}}}", type_display(k), type_display(v)), - TypeRef::Interface(n) => local_type_name(n).to_string(), - TypeRef::Named(_) => unreachable!("unresolved type reference"), + Ty::I8 => "i8".into(), + Ty::I16 => "i16".into(), + Ty::I32 => "i32".into(), + Ty::U8 => "u8".into(), + Ty::U16 => "u16".into(), + Ty::U32 => "u32".into(), + Ty::I64 => "i64".into(), + Ty::U64 => "u64".into(), + Ty::F32 => "f32".into(), + Ty::F64 => "f64".into(), + Ty::Bool => "bool".into(), + Ty::StringUtf8 | Ty::BorrowedStr => "string".into(), + Ty::Bytes | Ty::BorrowedBytes => "bytes".into(), + Ty::TypedHandle(_) | Ty::Handle => "handle".into(), + Ty::Record(n) | Ty::RichEnum(n) => local_type_name(n).to_string(), + Ty::Enum(n) => n.clone(), + Ty::Optional(inner) => format!("{}?", type_display(inner)), + Ty::List(inner) => format!("[{}]", type_display(inner)), + Ty::Iterator(inner) => format!("iter<{}>", type_display(inner)), + Ty::Map(k, v) => format!("{{{}:{}}}", type_display(k), type_display(v)), + Ty::Interface(n) => local_type_name(n).to_string(), } } /// The TS annotation of one IR type. -pub(crate) fn ts_type_for(ty: &TypeRef) -> String { +pub(crate) fn ts_type_for(ty: &Ty) -> String { match ty { - TypeRef::I8 - | TypeRef::I16 - | TypeRef::I32 - | TypeRef::U8 - | TypeRef::U16 - | TypeRef::U32 - | TypeRef::F32 - | TypeRef::F64 => "number".into(), - TypeRef::Bool => "boolean".into(), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => "string".into(), + Ty::I8 | Ty::I16 | Ty::I32 | Ty::U8 | Ty::U16 | Ty::U32 | Ty::F32 | Ty::F64 => { + "number".into() + } + Ty::Bool => "boolean".into(), + Ty::StringUtf8 | Ty::BorrowedStr => "string".into(), // Bytes cross the boundary as plain `Uint8Array` copies; the Node-only // `Buffer` type does not exist in browsers and is never returned here. - TypeRef::Bytes | TypeRef::BorrowedBytes => "Uint8Array".into(), + Ty::Bytes | Ty::BorrowedBytes => "Uint8Array".into(), // Every 64-bit integer crosses the JS boundary as a BigInt: wasm i64 // results arrive as BigInt and i64 arguments are BigInt-coerced. - TypeRef::I64 | TypeRef::U64 | TypeRef::Handle => "bigint".into(), + Ty::I64 | Ty::U64 | Ty::Handle => "bigint".into(), // A typed handle is an opaque i32 pointer at the ABI, surfaced as a // plain number. - TypeRef::TypedHandle(_) => "number".into(), + Ty::TypedHandle(_) => "number".into(), // Records, rich enums, plain enums, and interfaces surface as bare // local TS names; a cross-module reference (resolved to e.g. // `kv.Store`) must name the local `Store`, not the qualified IR name // which is undeclared here. - TypeRef::Enum(name) - | TypeRef::Record(name) - | TypeRef::RichEnum(name) - | TypeRef::Interface(name) => local_type_name(name).to_string(), - TypeRef::Optional(inner) => format!("{} | null", ts_type_for(inner)), - TypeRef::List(inner) => { + Ty::Enum(name) | Ty::Record(name) | Ty::RichEnum(name) | Ty::Interface(name) => { + local_type_name(name).to_string() + } + Ty::Optional(inner) => format!("{} | null", ts_type_for(inner)), + Ty::List(inner) => { let inner_ts = ts_type_for(inner); - if matches!(inner.as_ref(), TypeRef::Optional(_)) { + if matches!(inner.as_ref(), Ty::Optional(_)) { format!("({inner_ts})[]") } else { format!("{inner_ts}[]") @@ -148,82 +131,26 @@ pub(crate) fn ts_type_for(ty: &TypeRef) -> String { } // `iter` streams lazily; the wrapper is a JS iterator, never a // drained array. - TypeRef::Iterator(inner) => { + Ty::Iterator(inner) => { let t = ts_type_for(inner); format!("IterableIterator<{t}>") } - TypeRef::Map(k, v) => format!("Record<{}, {}>", ts_type_for(k), ts_type_for(v)), - TypeRef::Named(_) => unreachable!("unresolved type reference"), - } -} - -/// True if `ty` is one of the UTF-8 string spellings. -pub(crate) fn is_string_type(ty: &TypeRef) -> bool { - matches!(ty, TypeRef::StringUtf8 | TypeRef::BorrowedStr) -} - -/// Whether `ty` or any type nested inside it (optional payloads, list and -/// iterator elements, map keys/values) satisfies `pred`. -fn typeref_deep_any(ty: &TypeRef, pred: &dyn Fn(&TypeRef) -> bool) -> bool { - if pred(ty) { - return true; - } - match ty { - TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => { - typeref_deep_any(inner, pred) + // Maps decode into a plain object, whose keys are always strings at + // runtime; a `bigint` key would also be rejected by `Record`'s key + // constraint, so 64-bit keys are typed as the strings they become. + Ty::Map(k, v) => { + let key = match k.as_ref() { + Ty::I64 | Ty::U64 | Ty::Handle => "string".to_string(), + other => ts_type_for(other), + }; + format!("Record<{key}, {}>", ts_type_for(v)) } - TypeRef::Map(k, v) => typeref_deep_any(k, pred) || typeref_deep_any(v, pred), - _ => false, } } -/// Visit every boundary-crossing type in `api` (function, interface-member, -/// and callback params and returns; struct, variant, and error payload field -/// types), recursing into composite types, and return whether any satisfies -/// `pred`. -pub(crate) fn api_deep_any(api: &Api, pred: &dyn Fn(&TypeRef) -> bool) -> bool { - fn deep(ty: &TypeRef, pred: &dyn Fn(&TypeRef) -> bool) -> bool { - typeref_deep_any(ty, pred) - } - fn fn_any(f: &weaveffi_ir::ir::Function, pred: &dyn Fn(&TypeRef) -> bool) -> bool { - f.params.iter().any(|p| deep(&p.ty, pred)) - || f.returns.as_ref().is_some_and(|r| deep(r, pred)) - } - fn module_any(m: &Module, pred: &dyn Fn(&TypeRef) -> bool) -> bool { - m.functions.iter().any(|f| fn_any(f, pred)) - // Interface members marshal exactly like free functions. - || m.interfaces.iter().any(|i| { - i.constructors - .iter() - .chain(i.methods.iter()) - .chain(i.statics.iter()) - .any(|f| fn_any(f, pred)) - }) - || m - .structs - .iter() - .any(|s| s.fields.iter().any(|f| deep(&f.ty, pred))) - // Rich (algebraic) enums serialize their variant fields exactly - // like struct fields, so a string/bytes/list living only inside a - // variant payload still pulls in the corresponding helpers. - || m.enums.iter().any(|e| { - e.variants - .iter() - .any(|v| v.fields.iter().any(|f| deep(&f.ty, pred))) - }) - // Callback arguments are decoded by the listener trampolines. - || m.callbacks - .iter() - .any(|c| c.params.iter().any(|p| deep(&p.ty, pred))) - // Error payload fields are decoded from the error's value buffer. - || m.errors.as_ref().is_some_and(|d| { - d.codes - .iter() - .any(|c| c.fields.iter().any(|f| deep(&f.ty, pred))) - }) - || m.modules.iter().any(|sub| module_any(sub, pred)) - } - api.modules.iter().any(|m| module_any(m, pred)) +/// True if `ty` is one of the UTF-8 string spellings. +pub(crate) fn is_string_type(ty: &Ty) -> bool { + matches!(ty, Ty::StringUtf8 | Ty::BorrowedStr) } // ── Naming and error-surface policy ── diff --git a/crates/weaveffi-ir/src/ir.rs b/crates/weaveffi-ir/src/ir.rs index 56f9d986..2c55b29c 100644 --- a/crates/weaveffi-ir/src/ir.rs +++ b/crates/weaveffi-ir/src/ir.rs @@ -1,15 +1,23 @@ //! In-memory intermediate representation: the data model a parsed WeaveFFI IDL //! document becomes. //! -//! Backends read this tree, never the raw IDL text. [`Api`] is the root and -//! owns a forest of [`Module`]s, each grouping [`Function`]s, -//! [`InterfaceDef`]s, [`StructDef`]s, [`EnumDef`]s, [`CallbackDef`]s, -//! [`ListenerDef`]s, and an optional [`ErrorDomain`]. 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. - -use std::collections::BTreeMap; +//! This is the *document* model: [`Api`] is the root and owns a forest of +//! [`Module`]s, each grouping [`Function`]s, [`InterfaceDef`]s, [`StructDef`]s, +//! [`EnumDef`]s, [`CallbackDef`]s, [`ListenerDef`]s, and an optional +//! [`ErrorDomain`]. 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. +//! +//! The document model is deliberately *unresolved*: every user-defined type +//! reference is a [`TypeRef::Named`] carrying the name exactly as written. +//! Whether that name is a record, an enum, or an interface is decided by +//! `weaveffi-core`'s validator, which lowers the document into the resolved +//! binding model generators consume. Keeping the two representations distinct +//! means an IDL document always round-trips losslessly through this crate. +//! +//! Package identity and per-generator options are not part of the IDL: they +//! describe how bindings are *shipped*, not what the API *is*, and live in the +//! `weaveffi.toml` project configuration instead. use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -18,14 +26,14 @@ use serde::{Deserialize, Serialize}; /// generator expect. /// /// Pre-1.0 there is exactly one supported schema version: the current one. -/// Older schema revisions (0.1.0 through 0.5.0) are not accepted and have no -/// automated migration path: update the `version` field and adjust the -/// document to the current schema by hand. Post-1.0, schema bumps will ship -/// with a migration tool and [`SUPPORTED_VERSIONS`] will widen accordingly. +/// Older schema revisions are not accepted and have no automated migration +/// path: update the `version` field and adjust the document to the current +/// schema by hand. Post-1.0, schema bumps will ship with a migration tool and +/// [`SUPPORTED_VERSIONS`] will widen accordingly. /// /// 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.7.0"; +pub const CURRENT_SCHEMA_VERSION: &str = "0.8.0"; /// Every IR schema version the current tools accept. /// @@ -35,8 +43,8 @@ pub const CURRENT_SCHEMA_VERSION: &str = "0.7.0"; 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 format`/`extract` minimal by -/// omitting flags the user never set (e.g. `async: false`, `mutable: false`). +/// Keeps the canonical IDL emitted by `weaveffi extract` minimal by omitting +/// flags the user never set (e.g. `async: false`, `mutable: false`). #[allow(clippy::trivially_copy_pass_by_ref)] fn is_false(b: &bool) -> bool { !*b @@ -45,69 +53,18 @@ 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 -/// deserializes into (see [`crate::parse`]) and the single input every code -/// generator consumes. It pairs the schema version with the module forest, -/// optional package identity, and any per-generator overrides. -// `Eq` is omitted because `generators` holds `toml::Value`, which contains `f64`. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +/// 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, JsonSchema)] +#[serde(deny_unknown_fields)] #[schemars(description = "Top-level WeaveFFI API definition.")] pub struct Api { - /// IR schema version this document targets (for example `0.7.0`). + /// IR schema version this document targets (for example `0.8.0`). /// Validation rejects any value not listed in [`SUPPORTED_VERSIONS`]. pub version: String, - /// Package identity used to name, version, and describe every generated - /// consumer package (npm, PyPI, gem, NuGet, pub.dev, SwiftPM, Gradle, Go). - /// When omitted, generators fall back to the IDL file stem and version - /// `0.1.0`, but publishable artifacts should always set this explicitly. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub package: Option, /// Top-level modules that make up the API surface. Each is an independent /// namespace; modules may nest further through [`Module::modules`]. pub modules: Vec, - /// Per-generator configuration keyed by backend name (for example `swift` - /// or `python`). The opaque [`toml::Value`] payload is interpreted by each - /// generator, so unrecognized keys pass through untouched. `None` when the - /// IDL declares no `generators:` block. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[schemars(with = "Option>")] - pub generators: Option>, -} - -/// Package identity for the generated consumer artifacts. -/// -/// A single `package:` block in the IDL is the source of truth for the -/// name, version, and metadata stamped into every ecosystem manifest -/// (`package.json`, `pyproject.toml`, `*.gemspec`, `*.csproj`, `pubspec.yaml`, -/// `Package.swift`, `build.gradle`, `go.mod`). This is what makes the -/// generated packages standalone and publishable rather than all sharing a -/// placeholder identity. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[schemars(description = "Package identity for the generated consumer artifacts.")] -pub struct Package { - /// Canonical package name (e.g. `kvstore`). Per-target name overrides in - /// `generators:` (such as `python.package_name`) still take precedence. - pub name: String, - /// Semantic version stamped into every manifest (e.g. `1.2.0`). - pub version: String, - /// Short summary written into each manifest's description field. Omitted - /// from generated manifests when absent. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub description: Option, - /// License identifier, typically an SPDX expression such as `MIT` or - /// `Apache-2.0`, written into each manifest's license field. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub license: Option, - /// Package authors, each commonly formatted as `Name `, mapped to - /// whatever author or maintainer field the target ecosystem uses. Empty by - /// default. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub authors: Vec, - /// Project homepage URL recorded in manifests that expose one. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub homepage: Option, - /// Source repository URL recorded in manifests that expose one. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub repository: Option, } /// A module: a named namespace grouping related functions, types, callbacks, @@ -125,6 +82,11 @@ pub struct Module { /// Module name, used as a namespace segment and a symbol-prefix component /// in generated code (for example `contacts`). pub name: String, + /// Human-readable documentation for the module as a whole, propagated to + /// the generated namespace, class, or file header. `None` when + /// undocumented. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub doc: Option, /// Free functions this module exports across the FFI boundary. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub functions: Vec, @@ -226,6 +188,10 @@ pub struct InterfaceDef { /// `None` when undocumented. #[serde(default, skip_serializing_if = "Option::is_none")] pub doc: Option, + /// Deprecation notice for the whole type; when set, generators annotate + /// the emitted class. `None` means the interface is current. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub deprecated: Option, /// Constructors: static functions returning a new instance. A constructor /// declares no `return` (the instance is implicit) and may not be `async`. #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -296,7 +262,13 @@ pub struct ListenerDef { pub doc: Option, } -/// A reference to a type in the IDL. +/// A reference to a type in the IDL, exactly as written. +/// +/// Every user-defined type (record, enum, or interface) is a +/// [`Named`](Self::Named) reference at this level; the validator resolves the +/// name against the declarations in scope and lowers it into the resolved +/// kind generators consume. Keeping the document model unresolved is what +/// makes an IDL round-trip losslessly. /// /// Callback-style behavior is **not** expressed as a `TypeRef` variant. /// Instead, callbacks and listeners are declared at the module level via @@ -340,36 +312,10 @@ pub enum TypeRef { /// Opaque resource handle tagged with the name of what it refers to /// (`handle`), giving generators a distinct type per resource kind. TypedHandle(String), - /// An unresolved reference to a user-defined type, exactly as parsed. - /// - /// The parser cannot know whether a bare identifier names a record, an - /// enum, or an interface, so every user-type reference starts life as - /// `Named`. The resolution pass (`weaveffi_core::validate::resolve`) - /// rewrites each occurrence into [`Record`](Self::Record), - /// [`RichEnum`](Self::RichEnum), [`Enum`](Self::Enum), or - /// [`Interface`](Self::Interface); after a successful validate-and-resolve - /// no `Named` reference remains, and generators may treat one as a bug. + /// A reference to a user-defined type (record, enum, or interface) by its + /// bare or dot-qualified name (`Contact`, `shared.Status`), exactly as + /// parsed. Resolution happens in the validator, not here. Named(String), - /// 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 `weaveffi_free_bytes`) as a return. - 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. - Enum(String), - /// A user interface (see [`InterfaceDef`]): an opaque object reference. - /// As a parameter the object is borrowed for the call; as a return the - /// caller receives a new owned reference it must eventually release. - /// - /// The IDL spells an interface reference as its bare (or dotted-qualified) - /// name, exactly like a record; the resolution pass rewrites the parsed - /// [`Named`](Self::Named) into this variant when the name resolves to an - /// interface declaration. - Interface(String), /// Borrowed string slice (`&str`): a non-owning view valid only for the /// duration of a call, used to pass input without copying. BorrowedStr, @@ -380,8 +326,7 @@ pub enum TypeRef { Optional(Box), /// Homogeneous list (`[T]`) of the inner element type. List(Box), - /// Map (`{K:V}`) from a key type to a value type. Crosses the C ABI as - /// parallel key and value arrays. + /// 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. @@ -394,9 +339,7 @@ pub enum TypeRef { /// borrowed forms (`&str`, `&[u8]`), typed handles (`handle`), iterators /// (`iter`), lists (`[T]`), maps (`{K:V}`), and the optional suffix (`T?`). /// Any other bare identifier is taken to be a user-defined record, enum, or -/// interface name and returned as [`TypeRef::Named`]; the -/// record-versus-enum-versus-interface distinction is resolved later against -/// the module's declarations. +/// interface name and returned as [`TypeRef::Named`]. /// /// # Errors /// @@ -422,9 +365,8 @@ pub fn parse_type_ref(s: &str) -> Result { } if s.starts_with('{') && s.ends_with('}') { let inner = &s[1..s.len() - 1]; - let colon = inner - .find(':') - .ok_or_else(|| "map type missing ':' separator".to_string())?; + let colon = + map_separator(inner).ok_or_else(|| "map type missing ':' separator".to_string())?; let key = parse_type_ref(&inner[..colon])?; let val = parse_type_ref(&inner[colon + 1..])?; return Ok(TypeRef::Map(Box::new(key), Box::new(val))); @@ -465,32 +407,31 @@ pub fn parse_type_ref(s: &str) -> Result { } } +/// The byte offset of the top-level `:` separating a map's key from its value, +/// skipping any `:` nested inside a bracketed key such as `{ {a:b}: c }`. +fn map_separator(inner: &str) -> Option { + let mut depth = 0usize; + for (i, ch) in inner.char_indices() { + match ch { + '[' | '{' | '<' => depth += 1, + ']' | '}' | '>' => depth = depth.saturating_sub(1), + ':' if depth == 0 => return Some(i), + _ => {} + } + } + None +} + impl TypeRef { - /// The referenced user-type name for any user-defined reference variant - /// ([`Named`](Self::Named), [`Record`](Self::Record), - /// [`RichEnum`](Self::RichEnum), [`Enum`](Self::Enum), - /// [`Interface`](Self::Interface), or - /// [`TypedHandle`](Self::TypedHandle)), or `None` for every other type. + /// The referenced user-type name for a [`Named`](Self::Named) or + /// [`TypedHandle`](Self::TypedHandle) reference, or `None` for every + /// other type. pub fn user_name(&self) -> Option<&str> { match self { - TypeRef::Named(n) - | TypeRef::Record(n) - | TypeRef::RichEnum(n) - | TypeRef::Enum(n) - | TypeRef::Interface(n) - | TypeRef::TypedHandle(n) => Some(n), + TypeRef::Named(n) | TypeRef::TypedHandle(n) => Some(n), _ => None, } } - - /// `true` when this reference names a user-declared data type that - /// crosses the C ABI by value as a serialized buffer: a - /// [`Record`](Self::Record) or a [`RichEnum`](Self::RichEnum). Interfaces - /// are excluded because they cross as opaque object pointers with a - /// distinct ownership convention. - pub fn is_object_ref(&self) -> bool { - matches!(self, TypeRef::Record(_) | TypeRef::RichEnum(_)) - } } fn type_ref_to_string(ty: &TypeRef) -> String { @@ -512,11 +453,7 @@ fn type_ref_to_string(ty: &TypeRef) -> String { TypeRef::BorrowedBytes => "&[u8]".to_string(), TypeRef::Handle => "handle".to_string(), TypeRef::TypedHandle(name) => format!("handle<{name}>"), - TypeRef::Named(name) - | TypeRef::Record(name) - | TypeRef::RichEnum(name) - | TypeRef::Enum(name) - | TypeRef::Interface(name) => name.clone(), + TypeRef::Named(name) => name.clone(), TypeRef::Optional(inner) => format!("{}?", type_ref_to_string(inner)), TypeRef::List(inner) => format!("[{}]", type_ref_to_string(inner)), TypeRef::Map(k, v) => format!("{{{}:{}}}", type_ref_to_string(k), type_ref_to_string(v)), @@ -524,6 +461,12 @@ fn type_ref_to_string(ty: &TypeRef) -> String { } } +impl std::fmt::Display for TypeRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&type_ref_to_string(self)) + } +} + impl Serialize for TypeRef { fn serialize(&self, serializer: S) -> Result where @@ -546,7 +489,7 @@ impl<'de> Deserialize<'de> for TypeRef { /// Manual `JsonSchema` impl because `TypeRef` (de)serializes as a string with /// custom syntax: primitive names (`i32`, `string`, ...), `&str`, `&[u8]`, /// `handle<{name}>`, `iter<{T}>`, `[{T}]`, `{ {K}: {V} }`, `{name}?`, or any -/// user-defined struct/enum name. +/// user-defined struct/enum/interface name. impl JsonSchema for TypeRef { fn schema_name() -> String { "TypeRef".to_string() @@ -581,8 +524,8 @@ impl JsonSchema for TypeRef { /// [`is_rich`](Self::is_rich)). /// /// A C-style enum lowers across the C ABI by value as an integer, while an -/// algebraic enum lowers as an opaque object with a tag getter plus per-variant -/// constructors and field getters. +/// algebraic enum is a value type that crosses the ABI serialized in a value +/// buffer (an `i32` tag followed by the active variant's fields). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[schemars( description = "An enum type. C-style when every variant is a bare discriminant; an algebraic sum type when any variant declares fields." @@ -594,6 +537,10 @@ pub struct EnumDef { /// `None` when undocumented. #[serde(default, skip_serializing_if = "Option::is_none")] pub doc: Option, + /// Deprecation notice for the whole type; when set, generators annotate + /// the emitted enum. `None` means the enum is current. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub deprecated: Option, /// The variants in declaration order. Whether any of them carries fields /// decides if this is a C-style or an algebraic enum. pub variants: Vec, @@ -601,9 +548,8 @@ pub struct EnumDef { impl EnumDef { /// `true` when this is an *algebraic* enum (a sum type): at least one - /// variant carries associated data. Such enums lower across the C ABI as - /// opaque objects (a tag getter plus per-variant constructors and field - /// getters); a C-style enum (every variant a bare discriminant) lowers by + /// variant carries associated data. Such enums cross the C ABI as value + /// buffers; a C-style enum (every variant a bare discriminant) lowers by /// value as an integer. pub fn is_rich(&self) -> bool { self.variants.iter().any(|v| !v.fields.is_empty()) @@ -639,6 +585,10 @@ pub struct StructDef { /// `None` when undocumented. #[serde(default, skip_serializing_if = "Option::is_none")] pub doc: Option, + /// Deprecation notice for the whole type; when set, generators annotate + /// the emitted record. `None` means the struct is current. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub deprecated: Option, /// The fields in declaration order; order is preserved in the generated /// type, its constructors, and its serialized buffer encoding. pub fields: Vec, @@ -695,1067 +645,181 @@ pub struct ErrorCode { mod tests { use super::*; - #[test] - fn struct_def_round_trip_yaml() { - let yaml = r#" -version: "0.7.0" -modules: - - name: geometry - functions: [] - structs: - - name: Point - doc: "A 2D point" - fields: - - name: x - type: f64 - - name: "y" - type: f64 - doc: "Y coordinate" -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let m = &api.modules[0]; - assert_eq!(m.structs.len(), 1); - let s = &m.structs[0]; - assert_eq!(s.name, "Point"); - assert_eq!(s.doc.as_deref(), Some("A 2D point")); - assert_eq!(s.fields.len(), 2); - assert_eq!(s.fields[0].name, "x"); - assert_eq!(s.fields[0].ty, TypeRef::F64); - assert_eq!(s.fields[0].doc, None); - assert_eq!(s.fields[1].name, "y"); - assert_eq!(s.fields[1].doc.as_deref(), Some("Y coordinate")); - } - - #[test] - fn struct_def_round_trip_json() { - let json = r#"{ - "version": "0.7.0", - "modules": [{ - "name": "geo", - "functions": [], - "structs": [{ - "name": "Rect", - "fields": [ - {"name": "width", "type": "i32"}, - {"name": "height", "type": "i32"} - ] - }] - }] - }"#; - let api: Api = serde_json::from_str(json).unwrap(); - let s = &api.modules[0].structs[0]; - assert_eq!(s.name, "Rect"); - assert_eq!(s.doc, None); - assert_eq!(s.fields[0].ty, TypeRef::I32); - } - - #[test] - fn structs_default_to_empty() { - let yaml = r#" -version: "0.7.0" -modules: - - name: math - functions: [] -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - assert!(api.modules[0].structs.is_empty()); - } - - #[test] - fn package_block_round_trips_yaml() { - let yaml = r#" -version: "0.7.0" -package: - name: kvstore - version: 1.2.0 - description: "An embedded key/value store" - license: MIT - authors: - - "Ada Lovelace " - homepage: "https://example.com/kvstore" - repository: "https://github.com/example/kvstore" -modules: - - name: kv - functions: [] -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let pkg = api.package.as_ref().expect("package should parse"); - assert_eq!(pkg.name, "kvstore"); - assert_eq!(pkg.version, "1.2.0"); - assert_eq!( - pkg.description.as_deref(), - Some("An embedded key/value store") - ); - assert_eq!(pkg.license.as_deref(), Some("MIT")); - assert_eq!(pkg.authors, vec!["Ada Lovelace "]); - assert_eq!(pkg.homepage.as_deref(), Some("https://example.com/kvstore")); - assert_eq!( - pkg.repository.as_deref(), - Some("https://github.com/example/kvstore") - ); - - // Re-serialize and confirm the block survives the round trip. - let out = serde_yaml::to_string(&api).unwrap(); - assert!(out.contains("name: kvstore")); - assert!(out.contains("version: 1.2.0")); - } - - #[test] - fn package_is_optional() { - let yaml = r#" -version: "0.7.0" -modules: - - name: math - functions: [] -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - assert!(api.package.is_none()); - // Absent package must not appear in the canonical serialization. - let out = serde_yaml::to_string(&api).unwrap(); - assert!(!out.contains("package:")); - } - - #[test] - fn package_minimal_requires_name_and_version() { - let yaml = r#" -version: "0.7.0" -package: - name: tiny - version: 0.0.1 -modules: [] -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let pkg = api.package.as_ref().unwrap(); - assert_eq!(pkg.name, "tiny"); - assert_eq!(pkg.version, "0.0.1"); - assert!(pkg.description.is_none()); - assert!(pkg.authors.is_empty()); - } - - #[test] - fn typeref_struct_variant_serializes() { - let ty = TypeRef::Named("Point".to_string()); - let json = serde_json::to_string(&ty).unwrap(); - assert_eq!(json, r#""Point""#); - let back: TypeRef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, ty); - } - - #[test] - fn struct_field_with_struct_type() { - let field = StructField { - name: "origin".to_string(), - ty: TypeRef::Named("Point".to_string()), - doc: None, - }; - let json = serde_json::to_string(&field).unwrap(); - let back: StructField = serde_json::from_str(&json).unwrap(); - assert_eq!(back, field); - } - - #[test] - fn typeref_is_not_copy() { - let a = TypeRef::Named("Foo".to_string()); - let b = a.clone(); - assert_eq!(a, b); - } - - #[test] - fn enum_def_round_trip_yaml() { - let yaml = r#" -version: "0.7.0" -modules: - - name: graphics - functions: [] - enums: - - name: Color - doc: "Primary colors" - variants: - - name: Red - value: 0 - - name: Green - value: 1 - doc: "The color green" - - name: Blue - value: 2 -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let m = &api.modules[0]; - assert_eq!(m.enums.len(), 1); - let e = &m.enums[0]; - assert_eq!(e.name, "Color"); - assert_eq!(e.doc.as_deref(), Some("Primary colors")); - assert_eq!(e.variants.len(), 3); - assert_eq!(e.variants[0].name, "Red"); - assert_eq!(e.variants[0].value, 0); - assert_eq!(e.variants[0].doc, None); - assert_eq!(e.variants[1].name, "Green"); - assert_eq!(e.variants[1].value, 1); - assert_eq!(e.variants[1].doc.as_deref(), Some("The color green")); - assert_eq!(e.variants[2].name, "Blue"); - assert_eq!(e.variants[2].value, 2); - } - - #[test] - fn enum_def_round_trip_json() { - let json = r#"{ - "version": "0.7.0", - "modules": [{ - "name": "status", - "functions": [], - "enums": [{ - "name": "Status", - "variants": [ - {"name": "Ok", "value": 0}, - {"name": "Error", "value": 1} - ] - }] - }] - }"#; - let api: Api = serde_json::from_str(json).unwrap(); - let e = &api.modules[0].enums[0]; - assert_eq!(e.name, "Status"); - assert_eq!(e.doc, None); - assert_eq!(e.variants.len(), 2); - assert_eq!(e.variants[1].value, 1); - } - - #[test] - fn enums_default_to_empty() { - let yaml = r#" -version: "0.7.0" -modules: - - name: math - functions: [] -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - assert!(api.modules[0].enums.is_empty()); - } - - #[test] - fn typeref_enum_variant_serializes_as_name() { - let ty = TypeRef::Enum("Color".to_string()); - let json = serde_json::to_string(&ty).unwrap(); - assert_eq!(json, r#""Color""#); - } - - #[test] - fn enum_def_clone_and_eq() { - let e = EnumDef { - name: "Direction".to_string(), - doc: Some("Cardinal directions".to_string()), - variants: vec![ - EnumVariant { - name: "North".to_string(), - value: 0, - doc: None, - fields: vec![], - }, - EnumVariant { - name: "South".to_string(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }; - assert_eq!(e, e.clone()); - assert!(!e.is_rich()); - } - - #[test] - fn enum_def_is_rich_when_a_variant_has_fields() { - let e = EnumDef { - name: "Shape".to_string(), - doc: None, - variants: vec![ - EnumVariant { - name: "Circle".to_string(), - value: 0, - doc: None, - fields: vec![StructField { - name: "radius".to_string(), - ty: TypeRef::F64, - doc: None, - }], - }, - EnumVariant { - name: "Empty".to_string(), - value: 1, - doc: None, - fields: vec![], - }, - ], - }; - assert!(e.is_rich()); - } - - #[test] - fn struct_def_clone_and_eq() { - let s = StructDef { - name: "Color".to_string(), - doc: Some("RGB color".to_string()), - fields: vec![ - StructField { - name: "r".to_string(), - ty: TypeRef::U32, - doc: None, - }, - StructField { - name: "g".to_string(), - ty: TypeRef::U32, - doc: None, - }, - StructField { - name: "b".to_string(), - ty: TypeRef::U32, - doc: None, - }, - ], - }; - assert_eq!(s, s.clone()); - } - - #[test] - fn parse_type_ref_primitives() { - assert_eq!(parse_type_ref("i32"), Ok(TypeRef::I32)); - assert_eq!(parse_type_ref("u32"), Ok(TypeRef::U32)); - assert_eq!(parse_type_ref("i64"), Ok(TypeRef::I64)); - assert_eq!(parse_type_ref("f64"), Ok(TypeRef::F64)); - assert_eq!(parse_type_ref("bool"), Ok(TypeRef::Bool)); - assert_eq!(parse_type_ref("string"), Ok(TypeRef::StringUtf8)); - assert_eq!(parse_type_ref("bytes"), Ok(TypeRef::Bytes)); - assert_eq!(parse_type_ref("handle"), Ok(TypeRef::Handle)); - } - - #[test] - fn parse_type_ref_struct() { - assert_eq!( - parse_type_ref("Contact"), - Ok(TypeRef::Named("Contact".into())) - ); - assert_eq!( - parse_type_ref("MyWidget"), - Ok(TypeRef::Named("MyWidget".into())) - ); - } - - #[test] - fn parse_type_ref_optional() { - assert_eq!( - parse_type_ref("string?"), - Ok(TypeRef::Optional(Box::new(TypeRef::StringUtf8))) - ); - assert_eq!( - parse_type_ref("i32?"), - Ok(TypeRef::Optional(Box::new(TypeRef::I32))) - ); - assert_eq!( - parse_type_ref("Contact?"), - Ok(TypeRef::Optional(Box::new(TypeRef::Named( - "Contact".into() - )))) - ); + fn rt(s: &str) -> TypeRef { + let ty = parse_type_ref(s).unwrap_or_else(|e| panic!("{s}: {e}")); + assert_eq!(ty.to_string(), s, "type syntax must round-trip"); + ty + } + + #[test] + fn primitives_round_trip() { + let cases: &[(&str, TypeRef)] = &[ + ("i8", TypeRef::I8), + ("i16", TypeRef::I16), + ("i32", TypeRef::I32), + ("i64", TypeRef::I64), + ("u8", TypeRef::U8), + ("u16", TypeRef::U16), + ("u32", TypeRef::U32), + ("u64", TypeRef::U64), + ("f32", TypeRef::F32), + ("f64", TypeRef::F64), + ("bool", TypeRef::Bool), + ("string", TypeRef::StringUtf8), + ("bytes", TypeRef::Bytes), + ("handle", TypeRef::Handle), + ("&str", TypeRef::BorrowedStr), + ("&[u8]", TypeRef::BorrowedBytes), + ]; + for (s, expected) in cases { + assert_eq!(&rt(s), expected); + } } #[test] - fn parse_type_ref_list() { + fn composites_round_trip() { + assert_eq!(rt("Contact"), TypeRef::Named("Contact".into())); + assert_eq!(rt("shared.Status"), TypeRef::Named("shared.Status".into())); assert_eq!( - parse_type_ref("[i32]"), - Ok(TypeRef::List(Box::new(TypeRef::I32))) + rt("handle"), + TypeRef::TypedHandle("Session".into()) ); assert_eq!( - parse_type_ref("[string]"), - Ok(TypeRef::List(Box::new(TypeRef::StringUtf8))) + rt("Contact?"), + TypeRef::Optional(Box::new(TypeRef::Named("Contact".into()))) ); + assert_eq!(rt("[string]"), TypeRef::List(Box::new(TypeRef::StringUtf8))); assert_eq!( - parse_type_ref("[Contact]"), - Ok(TypeRef::List(Box::new(TypeRef::Named("Contact".into())))) + rt("{string:i32}"), + TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)) ); - } - - #[test] - fn parse_type_ref_u8_list_canonicalizes_to_bytes() { - assert_eq!(parse_type_ref("[u8]"), Ok(TypeRef::Bytes)); assert_eq!( - parse_type_ref("[u8]?"), - Ok(TypeRef::Optional(Box::new(TypeRef::Bytes))) + rt("iter"), + TypeRef::Iterator(Box::new(TypeRef::Named("Contact".into()))) ); - // Only the direct element position canonicalizes; nested u8 lists - // keep their list shape. assert_eq!( - parse_type_ref("[[u8]]"), - Ok(TypeRef::List(Box::new(TypeRef::Bytes))) - ); - } - - #[test] - fn parse_type_ref_nested() { - assert_eq!( - parse_type_ref("[i32?]"), - Ok(TypeRef::List(Box::new(TypeRef::Optional(Box::new( - TypeRef::I32 - ))))) - ); - assert_eq!( - parse_type_ref("[Contact]?"), - Ok(TypeRef::Optional(Box::new(TypeRef::List(Box::new( - TypeRef::Named("Contact".into()) - ))))) - ); - } - - #[test] - fn parse_type_ref_empty_is_error() { - assert!(parse_type_ref("").is_err()); - assert!(parse_type_ref(" ").is_err()); - } - - #[test] - fn typeref_primitive_round_trips() { - for ty in [ - TypeRef::I32, - TypeRef::U32, - TypeRef::I64, - TypeRef::F64, - TypeRef::Bool, - TypeRef::StringUtf8, - TypeRef::Bytes, - TypeRef::Handle, - ] { - let json = serde_json::to_string(&ty).unwrap(); - let back: TypeRef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, ty); - } - } - - #[test] - fn typeref_optional_round_trip() { - let ty = TypeRef::Optional(Box::new(TypeRef::StringUtf8)); - let json = serde_json::to_string(&ty).unwrap(); - assert_eq!(json, r#""string?""#); - let back: TypeRef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, ty); - } - - #[test] - fn typeref_list_round_trip() { - let ty = TypeRef::List(Box::new(TypeRef::I32)); - let json = serde_json::to_string(&ty).unwrap(); - assert_eq!(json, r#""[i32]""#); - let back: TypeRef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, ty); - } - - #[test] - fn typeref_optional_struct_round_trip() { - let ty = TypeRef::Optional(Box::new(TypeRef::Named("Contact".into()))); - let json = serde_json::to_string(&ty).unwrap(); - assert_eq!(json, r#""Contact?""#); - let back: TypeRef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, ty); - } - - #[test] - fn typeref_list_struct_round_trip() { - let ty = TypeRef::List(Box::new(TypeRef::Named("Contact".into()))); - let json = serde_json::to_string(&ty).unwrap(); - assert_eq!(json, r#""[Contact]""#); - let back: TypeRef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, ty); - } - - #[test] - fn typeref_optional_yaml_deser() { - let yaml = r#" -version: "0.7.0" -modules: - - name: contacts - functions: - - name: find - params: - - name: id - type: i32 - return: "Contact?" -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let f = &api.modules[0].functions[0]; - assert_eq!( - f.returns, - Some(TypeRef::Optional(Box::new(TypeRef::Named( - "Contact".into() - )))) - ); - } - - #[test] - fn typeref_list_yaml_deser() { - let yaml = r#" -version: "0.7.0" -modules: - - name: contacts - functions: - - name: list_all - params: [] - return: "[Contact]" -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let f = &api.modules[0].functions[0]; - assert_eq!( - f.returns, - Some(TypeRef::List(Box::new(TypeRef::Named("Contact".into())))) - ); - } - - #[test] - fn typeref_hash_works_with_box_variants() { - use std::collections::HashSet; - let mut set = HashSet::new(); - set.insert(TypeRef::I32); - set.insert(TypeRef::Optional(Box::new(TypeRef::I32))); - set.insert(TypeRef::List(Box::new(TypeRef::I32))); - set.insert(TypeRef::Optional(Box::new(TypeRef::Named("Foo".into())))); - set.insert(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )); - assert_eq!(set.len(), 5); - } - - #[test] - fn parse_type_ref_map_primitives() { - assert_eq!( - parse_type_ref("{string:i32}"), - Ok(TypeRef::Map( + rt("[{string:[i32?]}]"), + TypeRef::List(Box::new(TypeRef::Map( Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32) - )) + Box::new(TypeRef::List(Box::new(TypeRef::Optional(Box::new( + TypeRef::I32 + ))))) + ))) ); } #[test] - fn parse_type_ref_map_struct_value() { + fn u8_list_canonicalizes_to_bytes() { + assert_eq!(parse_type_ref("[u8]").unwrap(), TypeRef::Bytes); assert_eq!( - parse_type_ref("{string:Contact}"), - Ok(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::Named("Contact".into())) - )) + parse_type_ref("[[u8]]").unwrap(), + TypeRef::List(Box::new(TypeRef::Bytes)) ); } #[test] - fn parse_type_ref_map_nested_value() { + fn map_separator_skips_nested_colons() { assert_eq!( - parse_type_ref("{string:[i32]}"), - Ok(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::List(Box::new(TypeRef::I32))) - )) + parse_type_ref("{{string:i32}:bool}").unwrap(), + TypeRef::Map( + Box::new(TypeRef::Map( + Box::new(TypeRef::StringUtf8), + Box::new(TypeRef::I32) + )), + Box::new(TypeRef::Bool) + ) ); } #[test] - fn parse_type_ref_map_missing_colon() { + fn malformed_types_are_errors() { + assert!(parse_type_ref("").is_err()); + assert!(parse_type_ref(" ").is_err()); assert!(parse_type_ref("{string}").is_err()); + assert!(parse_type_ref("[]").is_err()); + assert!(parse_type_ref("?").is_err()); } #[test] - fn typeref_map_round_trip() { - let ty = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)); - let json = serde_json::to_string(&ty).unwrap(); - assert_eq!(json, r#""{string:i32}""#); - let back: TypeRef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, ty); - } - - #[test] - fn typeref_map_struct_round_trip() { - let ty = TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::Named("Contact".into())), - ); - let json = serde_json::to_string(&ty).unwrap(); - assert_eq!(json, r#""{string:Contact}""#); - let back: TypeRef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, ty); - } - - #[test] - fn typeref_map_yaml_deser() { + fn document_round_trips_through_yaml_and_json() { let yaml = r#" -version: "0.7.0" +version: "0.8.0" modules: - name: contacts + doc: Address book + structs: + - name: Contact + deprecated: use Person + fields: + - { name: name, type: string } + - { name: tags, type: "[string]" } + enums: + - name: Shape + variants: + - { name: Circle, value: 0, fields: [{ name: r, type: f64 }] } + - { name: Dot, value: 1 } + interfaces: + - name: Book + constructors: + - { name: open, params: [{ name: path, type: "&str" }], throws: true } + methods: + - { name: find, params: [{ name: q, type: string }], return: "Contact?" } + callbacks: + - { name: on_change, params: [{ name: id, type: i64 }] } + listeners: + - { name: changes, event_callback: on_change } + errors: + name: BookError + codes: + - { name: NotFound, code: 1, message: missing, fields: [{ name: id, type: i64 }] } functions: - - name: get_metadata + - name: count params: [] - return: "{string:i32}" + return: i64 + async: true + cancellable: true + deprecated: gone + since: "0.2.0" + modules: + - name: inner + functions: + - { name: ping, params: [], return: string } "#; let api: Api = serde_yaml::from_str(yaml).unwrap(); - let f = &api.modules[0].functions[0]; - assert_eq!( - f.returns, - Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32) - )) - ); - } - - #[test] - fn typeref_optional_map_round_trip() { - let ty = TypeRef::Optional(Box::new(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - ))); - let json = serde_json::to_string(&ty).unwrap(); - assert_eq!(json, r#""{string:i32}?""#); - let back: TypeRef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, ty); - } - - #[test] - fn parse_map_string_to_i32() { - assert_eq!( - parse_type_ref("{string:i32}"), - Ok(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )) - ); - } - - #[test] - fn parse_map_string_to_struct() { - assert_eq!( - parse_type_ref("{string:Contact}"), - Ok(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::Named("Contact".into())), - )) - ); - } - - #[test] - fn parse_map_roundtrip() { - let ty = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)); - let json = serde_json::to_string(&ty).unwrap(); - let back: TypeRef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, ty); - } - - #[test] - fn parse_optional_map() { - assert_eq!( - parse_type_ref("{string:i32}?"), - Ok(TypeRef::Optional(Box::new(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::I32), - )))) - ); - } - - #[test] - fn parse_map_of_lists() { - assert_eq!( - parse_type_ref("{string:[i32]}"), - Ok(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::List(Box::new(TypeRef::I32))), - )) - ); - } - - #[test] - fn parse_type_ref_iterator() { - assert_eq!( - parse_type_ref("iter"), - Ok(TypeRef::Iterator(Box::new(TypeRef::I32))) - ); - assert_eq!( - parse_type_ref("iter"), - Ok(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))) - ); + assert_eq!(api.version, CURRENT_SCHEMA_VERSION); + let m = &api.modules[0]; + assert_eq!(m.doc.as_deref(), Some("Address book")); + assert_eq!(m.structs[0].deprecated.as_deref(), Some("use Person")); + assert!(m.enums[0].is_rich()); assert_eq!( - parse_type_ref("iter"), - Ok(TypeRef::Iterator(Box::new(TypeRef::Named( + m.interfaces[0].methods[0].returns, + Some(TypeRef::Optional(Box::new(TypeRef::Named( "Contact".into() )))) ); - } - - #[test] - fn typeref_iterator_round_trip() { - let ty = TypeRef::Iterator(Box::new(TypeRef::I32)); - let json = serde_json::to_string(&ty).unwrap(); - assert_eq!(json, r#""iter""#); - let back: TypeRef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, ty); - } - - #[test] - fn typeref_iterator_struct_round_trip() { - let ty = TypeRef::Iterator(Box::new(TypeRef::Named("Contact".into()))); - let json = serde_json::to_string(&ty).unwrap(); - assert_eq!(json, r#""iter""#); - let back: TypeRef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, ty); - } - - #[test] - fn parse_type_ref_borrowed() { - assert_eq!(parse_type_ref("&str"), Ok(TypeRef::BorrowedStr)); - assert_eq!(parse_type_ref("&[u8]"), Ok(TypeRef::BorrowedBytes)); - } - - #[test] - fn typeref_borrowed_round_trip() { - for ty in [TypeRef::BorrowedStr, TypeRef::BorrowedBytes] { - let json = serde_json::to_string(&ty).unwrap(); - let back: TypeRef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, ty); - } - } - - #[test] - fn typeref_borrowed_str_serializes_as_ampersand_str() { - let json = serde_json::to_string(&TypeRef::BorrowedStr).unwrap(); - assert_eq!(json, r#""&str""#); - } - - #[test] - fn typeref_borrowed_bytes_serializes_as_ampersand_u8() { - let json = serde_json::to_string(&TypeRef::BorrowedBytes).unwrap(); - assert_eq!(json, r#""&[u8]""#); - } - - #[test] - fn typeref_borrowed_yaml_deser() { - let yaml = r#" -version: "0.7.0" -modules: - - name: io - functions: - - name: write - params: - - name: data - type: "&str" - - name: raw - type: "&[u8]" -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let f = &api.modules[0].functions[0]; - assert_eq!(f.params[0].ty, TypeRef::BorrowedStr); - assert_eq!(f.params[1].ty, TypeRef::BorrowedBytes); - } - - #[test] - fn parse_typed_handle() { - assert_eq!( - parse_type_ref("handle"), - Ok(TypeRef::TypedHandle("Session".into())) - ); - assert_eq!(parse_type_ref("handle"), Ok(TypeRef::Handle)); - } - - #[test] - fn generators_field_parses_from_yaml() { - let yaml = r#" -version: "0.7.0" -modules: - - name: math - functions: [] -generators: - swift: - module_name: MySwiftModule - android: - package: com.example.app -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let generators = api.generators.as_ref().unwrap(); - let swift = generators["swift"].as_table().unwrap(); - assert_eq!(swift["module_name"].as_str(), Some("MySwiftModule")); - let android = generators["android"].as_table().unwrap(); - assert_eq!(android["package"].as_str(), Some("com.example.app")); - } - - #[test] - fn generators_defaults_to_none() { - let yaml = r#" -version: "0.7.0" -modules: - - name: math - functions: [] -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - assert!(api.generators.is_none()); - } - - #[test] - fn parse_typed_handle_roundtrip() { - let ty = TypeRef::TypedHandle("Connection".into()); - let json = serde_json::to_string(&ty).unwrap(); - assert_eq!(json, r#""handle""#); - let back: TypeRef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, ty); - } - - #[test] - fn callback_def_round_trip_yaml() { - let yaml = r#" -version: "0.7.0" -modules: - - name: events - functions: [] - callbacks: - - name: on_data - params: - - name: payload - type: string - doc: "Fired when data arrives" -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let m = &api.modules[0]; - assert_eq!(m.callbacks.len(), 1); - let cb = &m.callbacks[0]; - assert_eq!(cb.name, "on_data"); - assert_eq!(cb.params.len(), 1); - assert_eq!(cb.params[0].name, "payload"); - assert_eq!(cb.params[0].ty, TypeRef::StringUtf8); - assert_eq!(cb.doc.as_deref(), Some("Fired when data arrives")); - } - - #[test] - fn listener_def_round_trip_yaml() { - let yaml = r#" -version: "0.7.0" -modules: - - name: events - functions: [] - callbacks: - - name: on_data - params: [] - listeners: - - name: data_stream - event_callback: on_data - doc: "Subscribe to data events" -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let m = &api.modules[0]; - assert_eq!(m.listeners.len(), 1); - let l = &m.listeners[0]; - assert_eq!(l.name, "data_stream"); - assert_eq!(l.event_callback, "on_data"); - assert_eq!(l.doc.as_deref(), Some("Subscribe to data events")); - } - - #[test] - fn callbacks_and_listeners_default_to_empty() { - let yaml = r#" -version: "0.7.0" -modules: - - name: math - functions: [] -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - assert!(api.modules[0].callbacks.is_empty()); - assert!(api.modules[0].listeners.is_empty()); - } - - #[test] - fn callback_def_json_round_trip() { - let cb = CallbackDef { - name: "on_event".to_string(), - params: vec![Param { - name: "data".to_string(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - doc: Some("event callback".to_string()), - }; - let json = serde_json::to_string(&cb).unwrap(); - let back: CallbackDef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, cb); - } - - #[test] - fn listener_def_json_round_trip() { - let l = ListenerDef { - name: "watcher".to_string(), - event_callback: "on_change".to_string(), - doc: None, - }; - let json = serde_json::to_string(&l).unwrap(); - let back: ListenerDef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, l); - } - - #[test] - fn error_code_fields_default_to_empty() { - let yaml = r#" -version: "0.7.0" -modules: - - name: kv - functions: [] - errors: - name: KvError - codes: - - name: NOT_FOUND - code: 1 - message: "key not found" -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let code = &api.modules[0].errors.as_ref().unwrap().codes[0]; - assert!(code.fields.is_empty()); - // Absent payload fields must not appear in the canonical serialization. - let out = serde_yaml::to_string(&api).unwrap(); - assert!(!out.contains("fields:")); - } - - #[test] - fn error_code_payload_fields_round_trip() { - let yaml = r#" -version: "0.7.0" -modules: - - name: kv - functions: [] - errors: - name: KvError - codes: - - name: QUOTA_EXCEEDED - code: 2 - message: "quota exceeded" - fields: - - name: used - type: u64 - - name: limit - type: u64 -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let code = &api.modules[0].errors.as_ref().unwrap().codes[0]; - assert_eq!(code.fields.len(), 2); - assert_eq!(code.fields[0].name, "used"); - assert_eq!(code.fields[0].ty, TypeRef::U64); - assert_eq!(code.fields[1].name, "limit"); + assert_eq!(m.errors.as_ref().unwrap().codes[0].fields.len(), 1); + assert!(m.functions[0].r#async && m.functions[0].cancellable); + assert_eq!(m.modules[0].functions[0].name, "ping"); let json = serde_json::to_string(&api).unwrap(); let back: Api = serde_json::from_str(&json).unwrap(); assert_eq!(back, api); - } - - #[test] - fn param_mutable_defaults_to_false() { - let yaml = r#" -version: "0.7.0" -modules: - - name: io - functions: - - name: write - params: - - name: data - type: string -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - assert!(!api.modules[0].functions[0].params[0].mutable); - } - - #[test] - fn param_mutable_true_round_trip() { - let yaml = r#" -version: "0.7.0" -modules: - - name: io - functions: - - name: fill_buffer - params: - - name: buf - type: bytes - mutable: true -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - assert!(api.modules[0].functions[0].params[0].mutable); - - let json = serde_json::to_string(&api).unwrap(); - let back: Api = serde_json::from_str(&json).unwrap(); - assert!(back.modules[0].functions[0].params[0].mutable); - } - - #[test] - fn param_mutable_false_explicit() { - let json = r#"{ - "version": "0.7.0", - "modules": [{ - "name": "io", - "functions": [{ - "name": "read", - "params": [{"name": "buf", "type": "bytes", "mutable": false}] - }] - }] - }"#; - let api: Api = serde_json::from_str(json).unwrap(); - assert!(!api.modules[0].functions[0].params[0].mutable); - } - - #[test] - fn deprecated_and_since_default_to_none() { - let yaml = r#" -version: "0.7.0" -modules: - - name: math - functions: - - name: add - params: [] -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let f = &api.modules[0].functions[0]; - assert_eq!(f.deprecated, None); - assert_eq!(f.since, None); - } - - #[test] - fn deprecated_and_since_round_trip() { - let yaml = r#" -version: "0.7.0" -modules: - - name: math - functions: - - name: add_old - params: [] - deprecated: "Use add_v2 instead" - since: "0.1.0" -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let f = &api.modules[0].functions[0]; - assert_eq!(f.deprecated.as_deref(), Some("Use add_v2 instead")); - assert_eq!(f.since.as_deref(), Some("0.1.0")); - - let json = serde_json::to_string(&api).unwrap(); - let back: Api = serde_json::from_str(&json).unwrap(); - let f2 = &back.modules[0].functions[0]; - assert_eq!(f2.deprecated.as_deref(), Some("Use add_v2 instead")); - assert_eq!(f2.since.as_deref(), Some("0.1.0")); + let yaml2 = serde_yaml::to_string(&api).unwrap(); + let back2: Api = serde_yaml::from_str(&yaml2).unwrap(); + assert_eq!(back2, api); } #[test] fn serialization_omits_defaulted_fields() { - // A minimal API whose every optional/defaulted field is at its - // default must serialize without emitting those fields, so the - // canonical IDL produced by `weaveffi format`/`extract` stays terse. let api = Api { - version: "0.7.0".into(), + version: CURRENT_SCHEMA_VERSION.into(), modules: vec![Module { - name: "calc".into(), + name: "m".into(), + doc: None, functions: vec![Function { - name: "add".into(), - params: vec![Param { - name: "a".into(), - ty: TypeRef::I32, - mutable: false, - doc: None, - }], - returns: Some(TypeRef::I32), + name: "f".into(), + params: vec![], + returns: None, doc: None, throws: false, r#async: false, @@ -1771,185 +835,23 @@ modules: errors: None, modules: vec![], }], - generators: None, - package: None, }; let yaml = serde_yaml::to_string(&api).unwrap(); - for needle in [ - "generators", - "interfaces", - "structs", - "enums", - "callbacks", - "listeners", - "errors", - "modules:\n", // nested module list (top-level key is "modules") - "doc", - "throws", - "async", - "cancellable", - "deprecated", - "since", - "mutable", - "null", - "[]", - "false", - ] { - // `modules:` appears once at the top level; assert the *nested* - // empty module list under a module is gone by checking it never - // shows an empty sequence. - if needle == "modules:\n" { - continue; - } + for noise in ["null", "[]", "false", "async", "throws", "doc", "params"] { assert!( - !yaml.contains(needle), - "default field `{needle}` leaked into canonical YAML:\n{yaml}" + !yaml.contains(noise), + "{noise} leaked into canonical form:\n{yaml}" ); } - // Round-trips back to an equal value. - let back: Api = serde_yaml::from_str(&yaml).unwrap(); - assert_eq!(back, api); - } - - #[test] - fn parse_type_ref_does_not_yield_callback() { - assert_eq!( - parse_type_ref("callback"), - Ok(TypeRef::Named("callback".into())) - ); } #[test] - fn api_json_schema_derives() { + fn json_schema_types_are_strings() { let schema = schemars::schema_for!(Api); let json = serde_json::to_value(&schema).unwrap(); - assert!(json.get("$schema").is_some()); - assert!(json.get("properties").is_some()); - assert_eq!(json.get("title").and_then(|v| v.as_str()), Some("Api")); - let defs = json - .get("definitions") - .and_then(|v| v.as_object()) - .expect("definitions"); - assert!(defs.contains_key("Module")); - assert!(defs.contains_key("Function")); - assert!(defs.contains_key("Param")); - assert!(defs.contains_key("TypeRef")); - assert!(defs.contains_key("InterfaceDef")); - assert!(defs.contains_key("StructDef")); - assert!(defs.contains_key("StructField")); - assert!(defs.contains_key("EnumDef")); - assert!(defs.contains_key("EnumVariant")); - assert!(defs.contains_key("CallbackDef")); - assert!(defs.contains_key("ListenerDef")); - assert!(defs.contains_key("ErrorDomain")); - assert!(defs.contains_key("ErrorCode")); - } - - #[test] - fn interface_round_trip_yaml() { - let yaml = r#" -version: "0.7.0" -modules: - - name: kv - interfaces: - - name: Store - doc: "A key/value store" - constructors: - - name: open - params: - - name: path - type: string - throws: true - methods: - - name: put - params: - - name: key - type: string - - name: value - type: bytes - return: bool - throws: true - - name: count - return: i64 - statics: - - name: default_path - return: string -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - let m = &api.modules[0]; - assert!(m.functions.is_empty()); - assert_eq!(m.interfaces.len(), 1); - let i = &m.interfaces[0]; - assert_eq!(i.name, "Store"); - assert_eq!(i.doc.as_deref(), Some("A key/value store")); - assert_eq!(i.constructors.len(), 1); - assert!(i.constructors[0].throws); - assert_eq!(i.constructors[0].returns, None); - assert_eq!(i.methods.len(), 2); - assert!(i.methods[0].throws); - assert!(!i.methods[1].throws); - assert_eq!(i.methods[1].returns, Some(TypeRef::I64)); - assert_eq!(i.statics.len(), 1); - assert_eq!(i.statics[0].returns, Some(TypeRef::StringUtf8)); - - // Round trip preserves the interface block. - let out = serde_yaml::to_string(&api).unwrap(); - let back: Api = serde_yaml::from_str(&out).unwrap(); - assert_eq!(back, api); - } - - #[test] - fn interfaces_default_to_empty() { - let yaml = r#" -version: "0.7.0" -modules: - - name: math - functions: [] -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - assert!(api.modules[0].interfaces.is_empty()); - } - - #[test] - fn throws_defaults_to_false_and_round_trips() { - let yaml = r#" -version: "0.7.0" -modules: - - name: kv - functions: - - name: open - params: [] - throws: true - - name: count - params: [] -"#; - let api: Api = serde_yaml::from_str(yaml).unwrap(); - assert!(api.modules[0].functions[0].throws); - assert!(!api.modules[0].functions[1].throws); - let json = serde_json::to_string(&api).unwrap(); - let back: Api = serde_json::from_str(&json).unwrap(); - assert!(back.modules[0].functions[0].throws); - assert!(!back.modules[0].functions[1].throws); - } - - #[test] - fn typeref_interface_serializes_as_name() { - let ty = TypeRef::Interface("Store".to_string()); - let json = serde_json::to_string(&ty).unwrap(); - assert_eq!(json, r#""Store""#); - // Deserialization yields `Named`; the resolver rewrites it later. - let back: TypeRef = serde_json::from_str(&json).unwrap(); - assert_eq!(back, TypeRef::Named("Store".into())); - } - - #[test] - fn typeref_json_schema_is_string_with_description() { - let schema = schemars::schema_for!(TypeRef); - let json = serde_json::to_value(&schema).unwrap(); - assert_eq!(json.get("type").and_then(|v| v.as_str()), Some("string")); - assert!(json - .get("description") - .and_then(|v| v.as_str()) - .is_some_and(|s| s.contains("handle<") && s.contains("iter<"))); + let type_ref = &json["definitions"]["TypeRef"]; + assert_eq!(type_ref["type"], "string"); + assert!(json["properties"].get("package").is_none()); + assert!(json["properties"].get("generators").is_none()); } } diff --git a/crates/weaveffi-ir/src/parse.rs b/crates/weaveffi-ir/src/parse.rs index 5d6f48ab..62c37ebc 100644 --- a/crates/weaveffi-ir/src/parse.rs +++ b/crates/weaveffi-ir/src/parse.rs @@ -170,13 +170,14 @@ pub fn parse_api_str(s: &str, format: &str) -> Result { #[cfg(test)] mod tests { use super::*; - use crate::ir::{Api, Function, Module, Param, TypeRef}; + use crate::ir::{Function, Module, Param, TypeRef, CURRENT_SCHEMA_VERSION}; fn expected_api() -> Api { Api { - version: "0.7.0".to_string(), + version: CURRENT_SCHEMA_VERSION.to_string(), modules: vec![Module { name: "math".to_string(), + doc: None, functions: vec![Function { name: "add".to_string(), params: vec![ @@ -209,15 +210,13 @@ mod tests { errors: None, modules: vec![], }], - generators: None, - package: None, } } #[test] - fn parse_yaml_round_trip() { + fn every_format_parses_the_same_document() { let yaml = r#" -version: "0.7.0" +version: "0.8.0" modules: - name: math functions: @@ -230,13 +229,8 @@ modules: return: i32 doc: "Adds two numbers" "#; - assert_eq!(parse_api_str(yaml, "yaml").unwrap(), expected_api()); - } - - #[test] - fn parse_json_round_trip() { let json = r#"{ - "version": "0.7.0", + "version": "0.8.0", "modules": [{ "name": "math", "functions": [{ @@ -250,13 +244,8 @@ modules: }] }] }"#; - assert_eq!(parse_api_str(json, "json").unwrap(), expected_api()); - } - - #[test] - fn parse_toml_round_trip() { let toml_str = r#" -version = "0.7.0" +version = "0.8.0" [[modules]] name = "math" @@ -274,1070 +263,54 @@ type = "i32" 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] - fn parse_missing_optional_fields() { - let yaml = r#" -version: "0.7.0" -modules: - - name: math - functions: - - name: noop - params: [] -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let f = &api.modules[0].functions[0]; - assert_eq!(f.returns, None); - assert_eq!(f.doc, None); - assert!(!f.r#async); - assert_eq!(api.modules[0].errors, None); - } - - #[test] - fn parse_contacts_sample() { - let yaml = std::fs::read_to_string("../../samples/contacts/contacts.yml").unwrap(); - let api = parse_api_str(&yaml, "yml").unwrap(); - assert_eq!(api.version, "0.7.0"); - assert_eq!(api.modules.len(), 1); - - let m = &api.modules[0]; - assert_eq!(m.name, "contacts"); - - assert_eq!(m.enums.len(), 1); - let e = &m.enums[0]; - assert_eq!(e.name, "ContactType"); - assert_eq!(e.variants.len(), 3); - assert_eq!(e.variants[0].name, "Personal"); - assert_eq!(e.variants[0].value, 0); - assert_eq!(e.variants[1].name, "Work"); - assert_eq!(e.variants[1].value, 1); - assert_eq!(e.variants[2].name, "Other"); - assert_eq!(e.variants[2].value, 2); - - assert_eq!(m.structs.len(), 1); - let s = &m.structs[0]; - assert_eq!(s.name, "Contact"); - assert_eq!(s.fields.len(), 5); - assert_eq!(s.fields[0].ty, TypeRef::I64); - assert_eq!( - s.fields[3].ty, - TypeRef::Optional(Box::new(TypeRef::StringUtf8)) - ); - assert_eq!(s.fields[4].ty, TypeRef::Named("ContactType".to_string())); - - // The 0.7.0 surface replaces the handle-based free functions with the - // ContactBook interface and a typed error domain. - assert!(m.functions.is_empty()); - - let errors = m.errors.as_ref().expect("ContactsError domain"); - assert_eq!(errors.name, "ContactsError"); - assert_eq!(errors.codes.len(), 2); - assert_eq!(errors.codes[0].name, "InvalidName"); - assert_eq!(errors.codes[0].code, 1); - assert_eq!(errors.codes[0].message, "name must not be empty"); - assert_eq!(errors.codes[1].name, "NotFound"); - assert_eq!(errors.codes[1].code, 2); - assert_eq!(errors.codes[1].message, "contact not found"); - - assert_eq!(m.interfaces.len(), 1); - let book = &m.interfaces[0]; - assert_eq!(book.name, "ContactBook"); - assert_eq!(book.constructors.len(), 1); - assert_eq!(book.constructors[0].name, "new"); - assert!(book.constructors[0].params.is_empty()); - assert_eq!(book.constructors[0].returns, None); - assert!(!book.constructors[0].throws); - assert!(book.statics.is_empty()); - - assert_eq!(book.methods.len(), 5); - let add = &book.methods[0]; - assert_eq!(add.name, "add"); - assert_eq!(add.params.len(), 4); - assert_eq!( - add.params[2].ty, - TypeRef::Optional(Box::new(TypeRef::StringUtf8)) - ); - assert_eq!(add.params[3].ty, TypeRef::Named("ContactType".to_string())); - assert_eq!(add.returns, Some(TypeRef::Named("Contact".to_string()))); - assert!(add.throws); - let get = &book.methods[1]; - assert_eq!(get.name, "get"); - assert_eq!(get.params[0].ty, TypeRef::I64); - assert!(get.throws); - let list = &book.methods[2]; - assert_eq!(list.name, "list"); - assert_eq!( - list.returns, - Some(TypeRef::List(Box::new(TypeRef::Named( - "Contact".to_string() - )))) - ); - assert!(!list.throws); - assert_eq!(book.methods[3].name, "remove"); - assert_eq!(book.methods[3].returns, Some(TypeRef::Bool)); - assert_eq!(book.methods[4].name, "count"); - assert_eq!(book.methods[4].returns, Some(TypeRef::I32)); - } - #[test] fn unsupported_format_returns_error() { - let err = parse_api_str("{}", "xml").unwrap_err(); - assert!(matches!(err, ParseError::UnsupportedFormat(f) if f == "xml")); + assert!(matches!( + parse_api_str("", "xml"), + Err(ParseError::UnsupportedFormat(f)) if f == "xml" + )); } #[test] - fn line_col_to_offset_basic() { - let src = "abc\ndefg\nhi"; + fn line_col_to_offset_maps_and_clamps() { + let src = "ab\ncd\nef"; assert_eq!(line_col_to_offset(src, 1, 1), 0); - assert_eq!(line_col_to_offset(src, 1, 3), 2); - assert_eq!(line_col_to_offset(src, 2, 1), 4); - assert_eq!(line_col_to_offset(src, 2, 4), 7); - assert_eq!(line_col_to_offset(src, 3, 2), 10); - } - - #[test] - fn line_col_to_offset_unknown_returns_zero() { - assert_eq!(line_col_to_offset("anything", 0, 0), 0); - assert_eq!(line_col_to_offset("anything", 0, 5), 0); - } - - #[test] - fn line_col_to_offset_clamps_past_end() { - let src = "abc"; - assert_eq!(line_col_to_offset(src, 99, 99), src.len()); + assert_eq!(line_col_to_offset(src, 2, 2), 4); + assert_eq!(line_col_to_offset(src, 0, 0), 0); + assert_eq!(line_col_to_offset(src, 99, 1), src.len()); + assert_eq!(line_col_to_offset(src, 1, 99), 2); } #[test] - fn yaml_parse_error_carries_span() { - let bad = "version: [unterminated\n"; - let err = parse_api_str(bad, "yaml").unwrap_err(); - match err { - ParseError::Yaml { - line, column, span, .. - } => { - assert!(line >= 1); - assert!(column >= 1); - assert!(span.is_some(), "expected span on yaml error"); + fn parse_errors_carry_spans() { + let yaml = "version: \"0.8.0\"\nmodules:\n - name: [oops\n"; + match parse_api_str(yaml, "yaml").unwrap_err() { + ParseError::Yaml { line, span, .. } => { + assert!(line > 0); + assert!(span.is_some()); } - other => panic!("expected ParseError::Yaml, got {other:?}"), + other => panic!("expected YAML error, got {other:?}"), } - } - - #[test] - fn json_parse_error_carries_span() { - let bad = "{ invalid }"; - let err = parse_api_str(bad, "json").unwrap_err(); - assert!(matches!(err, ParseError::Json { .. })); - } - - #[test] - fn toml_parse_error_carries_source() { - let bad = "version = [unterminated\n"; - let err = parse_api_str(bad, "toml").unwrap_err(); - match err { - ParseError::Toml { src, .. } => { - assert!(!src.is_empty()); - } - other => panic!("expected ParseError::Toml, got {other:?}"), + match parse_api_str("{\"version\": }", "json").unwrap_err() { + 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 parse_struct_definitions() { - let yaml = r#" -version: "0.7.0" -modules: - - name: contacts - functions: [] - structs: - - name: Person - fields: - - name: name - type: string - - name: age - type: i32 - - name: active - type: bool -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let s = &api.modules[0].structs[0]; - assert_eq!(s.name, "Person"); - assert_eq!(s.fields.len(), 3); - assert_eq!(s.fields[0].name, "name"); - assert_eq!(s.fields[0].ty, TypeRef::StringUtf8); - assert_eq!(s.fields[1].name, "age"); - assert_eq!(s.fields[1].ty, TypeRef::I32); - assert_eq!(s.fields[2].name, "active"); - assert_eq!(s.fields[2].ty, TypeRef::Bool); - } - - #[test] - fn parse_enum_definitions() { - let yaml = r#" -version: "0.7.0" -modules: - - name: colors - functions: [] - enums: - - name: Color - variants: - - name: Red - value: 0 - - name: Green - value: 1 - - name: Blue - value: 2 -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let e = &api.modules[0].enums[0]; - assert_eq!(e.name, "Color"); - assert_eq!(e.variants.len(), 3); - assert_eq!(e.variants[0].name, "Red"); - assert_eq!(e.variants[0].value, 0); - assert_eq!(e.variants[1].name, "Green"); - assert_eq!(e.variants[1].value, 1); - assert_eq!(e.variants[2].name, "Blue"); - assert_eq!(e.variants[2].value, 2); - } - - #[test] - fn parse_optional_types() { - let yaml = r#" -version: "0.7.0" -modules: - - name: ops - functions: - - name: maybe - params: - - name: label - type: "string?" - - name: count - type: "i32?" -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let params = &api.modules[0].functions[0].params; - assert_eq!( - params[0].ty, - TypeRef::Optional(Box::new(TypeRef::StringUtf8)) - ); - assert_eq!(params[1].ty, TypeRef::Optional(Box::new(TypeRef::I32))); - } - - #[test] - fn parse_list_types() { - let yaml = r#" -version: "0.7.0" -modules: - - name: ops - functions: - - name: batch - params: - - name: ids - type: "[i32]" - - name: names - type: "[string]" -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let params = &api.modules[0].functions[0].params; - assert_eq!(params[0].ty, TypeRef::List(Box::new(TypeRef::I32))); - assert_eq!(params[1].ty, TypeRef::List(Box::new(TypeRef::StringUtf8))); - } - - #[test] - fn parse_struct_ref_in_function() { - let yaml = r#" -version: "0.7.0" -modules: - - name: contacts - functions: - - name: save - params: - - name: contact - type: Contact - structs: - - name: Contact - fields: - - name: name - type: string -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let param = &api.modules[0].functions[0].params[0]; - assert_eq!(param.name, "contact"); - assert_eq!(param.ty, TypeRef::Named("Contact".to_string())); - } - - #[test] - fn parse_complex_nested_types() { - let yaml = r#" -version: "0.7.0" -modules: - - name: ops - functions: - - name: complex - params: - - name: opt_contacts - type: "[Contact?]" - - name: maybe_ids - type: "[i32]?" -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let params = &api.modules[0].functions[0].params; - assert_eq!( - params[0].ty, - TypeRef::List(Box::new(TypeRef::Optional(Box::new(TypeRef::Named( - "Contact".to_string() - ))))) - ); - assert_eq!( - params[1].ty, - TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::I32)))) - ); - } - - #[test] - fn doc_example_primitives() { - let yaml = r#" -version: "0.7.0" -modules: - - name: demo - functions: - - name: add - params: - - { name: a, type: i32 } - - { name: b, type: i32 } - return: i32 - - name: scale - params: - - { name: value, type: f64 } - - { name: factor, type: f64 } - return: f64 - - name: count - params: - - { name: limit, type: u32 } - return: u32 - - name: timestamp - params: [] - return: i64 - - name: is_valid - params: - - { name: token, type: string } - return: bool - - name: echo - params: - - { name: message, type: string } - return: string - - name: compress - params: - - { name: data, type: bytes } - return: bytes - - name: open_resource - params: - - { name: path, type: string } - return: handle - - name: close_resource - params: - - { name: id, type: handle } -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let fns = &api.modules[0].functions; - assert_eq!(fns.len(), 9); - assert_eq!(fns[0].params[0].ty, TypeRef::I32); - assert_eq!(fns[0].returns, Some(TypeRef::I32)); - assert_eq!(fns[1].params[0].ty, TypeRef::F64); - assert_eq!(fns[1].returns, Some(TypeRef::F64)); - assert_eq!(fns[2].params[0].ty, TypeRef::U32); - assert_eq!(fns[2].returns, Some(TypeRef::U32)); - assert_eq!(fns[3].returns, Some(TypeRef::I64)); - assert_eq!(fns[4].params[0].ty, TypeRef::StringUtf8); - assert_eq!(fns[4].returns, Some(TypeRef::Bool)); - assert_eq!(fns[5].returns, Some(TypeRef::StringUtf8)); - assert_eq!(fns[6].params[0].ty, TypeRef::Bytes); - assert_eq!(fns[6].returns, Some(TypeRef::Bytes)); - assert_eq!(fns[7].returns, Some(TypeRef::Handle)); - assert_eq!(fns[8].params[0].ty, TypeRef::Handle); - assert_eq!(fns[8].returns, None); - } - - #[test] - fn doc_example_structs() { - let yaml = r#" -version: "0.7.0" -modules: - - name: geometry - structs: - - name: Point - doc: "A 2D point in space" - fields: - - name: x - type: f64 - doc: "X coordinate" - - name: "y" - type: f64 - doc: "Y coordinate" - - name: Rect - fields: - - name: origin - type: Point - - name: width - type: f64 - - name: height - type: f64 - functions: - - name: distance - params: - - { name: a, type: Point } - - { name: b, type: Point } - return: f64 - - name: bounding_box - params: - - { name: points, type: "[Point]" } - return: Rect -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let m = &api.modules[0]; - assert_eq!(m.structs.len(), 2); - assert_eq!(m.structs[0].name, "Point"); - assert_eq!(m.structs[0].doc.as_deref(), Some("A 2D point in space")); - assert_eq!(m.structs[0].fields[0].doc.as_deref(), Some("X coordinate")); - assert_eq!(m.structs[1].name, "Rect"); - assert_eq!( - m.structs[1].fields[0].ty, - TypeRef::Named("Point".to_string()) - ); - - assert_eq!(m.functions[0].params[0].ty, TypeRef::Named("Point".into())); - assert_eq!(m.functions[0].returns, Some(TypeRef::F64)); - assert_eq!( - m.functions[1].params[0].ty, - TypeRef::List(Box::new(TypeRef::Named("Point".into()))) - ); - assert_eq!(m.functions[1].returns, Some(TypeRef::Named("Rect".into()))); - } - - #[test] - fn doc_example_enums() { - let yaml = r#" -version: "0.7.0" -modules: - - name: contacts - enums: - - name: ContactType - doc: "Category of contact" - variants: - - name: Personal - value: 0 - doc: "Friends and family" - - name: Work - value: 1 - doc: "Professional contacts" - - name: Other - value: 2 - functions: - - name: count_by_type - params: - - { name: contact_type, type: ContactType } - return: i32 -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let m = &api.modules[0]; - assert_eq!(m.enums.len(), 1); - let e = &m.enums[0]; - assert_eq!(e.name, "ContactType"); - assert_eq!(e.doc.as_deref(), Some("Category of contact")); - assert_eq!(e.variants.len(), 3); - assert_eq!(e.variants[0].doc.as_deref(), Some("Friends and family")); - assert_eq!(e.variants[2].doc, None); - assert_eq!( - m.functions[0].params[0].ty, - TypeRef::Named("ContactType".into()) - ); - } - - #[test] - fn doc_example_optionals() { - let yaml = r#" -version: "0.7.0" -modules: - - name: contacts - structs: - - name: Contact - fields: - - { name: id, type: i64 } - - { name: name, type: string } - - { name: email, type: "string?" } - - { name: nickname, type: "string?" } - functions: - - name: find_contact - params: - - { name: id, type: i64 } - return: "Contact?" - doc: "Returns null if no contact exists with the given id" - - name: update_email - params: - - { name: id, type: i64 } - - { name: email, type: "string?" } -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let m = &api.modules[0]; - let s = &m.structs[0]; - assert_eq!( - s.fields[2].ty, - TypeRef::Optional(Box::new(TypeRef::StringUtf8)) - ); - assert_eq!( - m.functions[0].returns, - Some(TypeRef::Optional(Box::new(TypeRef::Named( - "Contact".into() - )))) - ); - assert_eq!( - m.functions[1].params[1].ty, - TypeRef::Optional(Box::new(TypeRef::StringUtf8)) - ); - } - - #[test] - fn doc_example_lists() { - let yaml = r#" -version: "0.7.0" -modules: - - name: ops - structs: - - name: Contact - fields: - - { name: name, type: string } - functions: - - name: sum - params: - - { name: values, type: "[i32]" } - return: i32 - - name: list_contacts - params: [] - return: "[Contact]" - - name: batch_delete - params: - - { name: ids, type: "[i64]" } - return: i32 -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let fns = &api.modules[0].functions; - assert_eq!(fns[0].params[0].ty, TypeRef::List(Box::new(TypeRef::I32))); - assert_eq!( - fns[1].returns, - Some(TypeRef::List(Box::new(TypeRef::Named("Contact".into())))) - ); - assert_eq!(fns[2].params[0].ty, TypeRef::List(Box::new(TypeRef::I64))); - } - - #[test] - fn doc_example_nested_types() { - let yaml = r#" -version: "0.7.0" -modules: - - name: ops - structs: - - name: Contact - fields: - - { name: name, type: string } - functions: - - name: search - params: - - { name: query, type: string } - return: "[Contact?]" - - name: get_scores - params: - - { name: user_id, type: i64 } - return: "[i32]?" - - name: bulk_update - params: - - { name: emails, type: "[string?]" } - return: i32 -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let fns = &api.modules[0].functions; - assert_eq!( - fns[0].returns, - Some(TypeRef::List(Box::new(TypeRef::Optional(Box::new( - TypeRef::Named("Contact".into()) - ))))) - ); - assert_eq!( - fns[1].returns, - Some(TypeRef::Optional(Box::new(TypeRef::List(Box::new( - TypeRef::I32 - ))))) - ); - assert_eq!( - fns[2].params[0].ty, - TypeRef::List(Box::new(TypeRef::Optional(Box::new(TypeRef::StringUtf8)))) - ); - } - - #[test] - fn doc_example_error_domain() { - let yaml = r#" -version: "0.7.0" -modules: - - name: contacts - errors: - name: ContactErrors - codes: - - name: not_found - code: 1 - message: "Contact not found" - - name: duplicate - code: 2 - message: "Contact already exists" - - name: invalid_email - code: 3 - message: "Email address is invalid" - functions: - - name: create_contact - params: - - { name: name, type: string } - - { name: email, type: string } - return: handle - - name: get_contact - params: - - { name: id, type: handle } - return: string -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let m = &api.modules[0]; - assert_eq!(m.name, "contacts"); - - let errors = m.errors.as_ref().expect("error domain should be present"); - assert_eq!(errors.name, "ContactErrors"); - assert_eq!(errors.codes.len(), 3); - assert_eq!(errors.codes[0].name, "not_found"); - assert_eq!(errors.codes[0].code, 1); - assert_eq!(errors.codes[0].message, "Contact not found"); - assert_eq!(errors.codes[1].name, "duplicate"); - assert_eq!(errors.codes[1].code, 2); - assert_eq!(errors.codes[1].message, "Contact already exists"); - assert_eq!(errors.codes[2].name, "invalid_email"); - assert_eq!(errors.codes[2].code, 3); - assert_eq!(errors.codes[2].message, "Email address is invalid"); - - assert_eq!(m.functions.len(), 2); - assert_eq!(m.functions[0].name, "create_contact"); - assert_eq!(m.functions[0].returns, Some(TypeRef::Handle)); - assert_eq!(m.functions[1].name, "get_contact"); - assert_eq!(m.functions[1].returns, Some(TypeRef::StringUtf8)); - } - - #[test] - fn doc_example_complete_contacts() { - let yaml = r#" -version: "0.7.0" -modules: - - name: contacts - enums: - - name: ContactType - variants: - - { name: Personal, value: 0 } - - { name: Work, value: 1 } - - { name: Other, value: 2 } - structs: - - name: Contact - doc: "A contact record" - fields: - - { name: id, type: i64 } - - { name: first_name, type: string } - - { name: last_name, type: string } - - { name: email, type: "string?" } - - { name: contact_type, type: ContactType } - functions: - - name: create_contact - params: - - { name: first_name, type: string } - - { name: last_name, type: string } - - { name: email, type: "string?" } - - { name: contact_type, type: ContactType } - return: handle - - name: get_contact - params: - - { name: id, type: handle } - return: Contact - - name: list_contacts - params: [] - return: "[Contact]" - - name: delete_contact - params: - - { name: id, type: handle } - return: bool - - name: count_contacts - params: [] - return: i32 -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - assert_eq!(api.version, "0.7.0"); - let m = &api.modules[0]; - assert_eq!(m.name, "contacts"); - assert_eq!(m.enums.len(), 1); - assert_eq!(m.enums[0].variants.len(), 3); - assert_eq!(m.structs.len(), 1); - assert_eq!(m.structs[0].fields.len(), 5); - assert_eq!(m.functions.len(), 5); - assert_eq!(m.functions[0].returns, Some(TypeRef::Handle)); - assert_eq!( - m.functions[1].returns, - Some(TypeRef::Named("Contact".into())) - ); - assert_eq!( - m.functions[2].returns, - Some(TypeRef::List(Box::new(TypeRef::Named("Contact".into())))) - ); - assert_eq!(m.functions[3].returns, Some(TypeRef::Bool)); - assert_eq!(m.functions[4].returns, Some(TypeRef::I32)); - } - - #[test] - fn parse_function_with_map_param() { - let yaml = r#" -version: "0.7.0" -modules: - - name: store - functions: - - name: put_scores - params: - - name: scores - type: "{string:i32}" -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let param = &api.modules[0].functions[0].params[0]; - assert_eq!(param.name, "scores"); - assert_eq!( - param.ty, - TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)) - ); - } - - #[test] - fn parse_function_with_map_return() { - let yaml = r#" -version: "0.7.0" -modules: - - name: store - structs: - - name: Contact - fields: - - { name: name, type: string } - functions: - - name: get_contacts - params: [] - return: "{string:Contact}" -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let f = &api.modules[0].functions[0]; - assert_eq!(f.name, "get_contacts"); - assert_eq!( - f.returns, - Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::Named("Contact".into())) - )) - ); - } - - #[test] - fn parse_inventory_sample() { - let yaml = std::fs::read_to_string("../../samples/inventory/inventory.yml").unwrap(); - let api = parse_api_str(&yaml, "yml").unwrap(); - assert_eq!(api.version, "0.7.0"); - assert_eq!(api.modules.len(), 2); - - let products = &api.modules[0]; - assert_eq!(products.name, "products"); - assert_eq!(products.enums.len(), 1); - assert_eq!(products.structs.len(), 1); - // The 0.7.0 surface replaces the handle-based free functions with the - // Catalog interface. - assert!(products.functions.is_empty()); - assert_eq!(products.interfaces.len(), 1); - - let cat = &products.enums[0]; - assert_eq!(cat.name, "Category"); - assert_eq!(cat.variants.len(), 4); - assert_eq!(cat.variants[0].name, "Electronics"); - assert_eq!(cat.variants[0].value, 0); - assert_eq!(cat.variants[3].name, "Books"); - assert_eq!(cat.variants[3].value, 3); - - let product = &products.structs[0]; - assert_eq!(product.name, "Product"); - assert_eq!(product.fields.len(), 6); - assert_eq!(product.fields[0].ty, TypeRef::I64); - assert_eq!(product.fields[1].ty, TypeRef::StringUtf8); - assert_eq!( - product.fields[2].ty, - TypeRef::Optional(Box::new(TypeRef::StringUtf8)) - ); - assert_eq!(product.fields[3].ty, TypeRef::F64); - assert_eq!(product.fields[4].ty, TypeRef::Named("Category".to_string())); - assert_eq!( - product.fields[5].ty, - TypeRef::List(Box::new(TypeRef::StringUtf8)) - ); - - let products_errors = products.errors.as_ref().expect("ProductsError domain"); - assert_eq!(products_errors.name, "ProductsError"); - assert_eq!(products_errors.codes.len(), 2); - assert_eq!(products_errors.codes[0].name, "InvalidPrice"); - assert_eq!(products_errors.codes[0].code, 1); - assert_eq!(products_errors.codes[1].name, "ProductNotFound"); - assert_eq!(products_errors.codes[1].code, 2); - - let catalog = &products.interfaces[0]; - assert_eq!(catalog.name, "Catalog"); - assert_eq!(catalog.constructors.len(), 1); - assert_eq!(catalog.constructors[0].name, "new"); - assert!(catalog.statics.is_empty()); - assert_eq!(catalog.methods.len(), 5); - let add_product = &catalog.methods[0]; - assert_eq!(add_product.name, "add_product"); - assert_eq!( - add_product.returns, - Some(TypeRef::Named("Product".to_string())) - ); - assert!(add_product.throws); - let get_product = &catalog.methods[1]; - assert_eq!(get_product.name, "get_product"); - assert_eq!(get_product.params[0].ty, TypeRef::I64); - assert!(get_product.throws); - let search = &catalog.methods[2]; - assert_eq!(search.name, "search"); - assert_eq!( - search.returns, - Some(TypeRef::List(Box::new(TypeRef::Named( - "Product".to_string() - )))) - ); - assert!(!search.throws); - let update_price = &catalog.methods[3]; - assert_eq!(update_price.name, "update_price"); - assert_eq!(update_price.returns, Some(TypeRef::Bool)); - assert!(update_price.throws); - let remove = &catalog.methods[4]; - assert_eq!(remove.name, "remove"); - assert_eq!(remove.returns, Some(TypeRef::Bool)); - assert!(!remove.throws); - - let orders = &api.modules[1]; - assert_eq!(orders.name, "orders"); - assert_eq!(orders.enums.len(), 0); - assert_eq!(orders.structs.len(), 2); - assert_eq!(orders.functions.len(), 4); - - let orders_errors = orders.errors.as_ref().expect("OrdersError domain"); - assert_eq!(orders_errors.name, "OrdersError"); - assert_eq!(orders_errors.codes.len(), 2); - assert_eq!(orders_errors.codes[0].name, "OrderNotFound"); - assert_eq!(orders_errors.codes[0].code, 1); - assert_eq!(orders_errors.codes[1].name, "EmptyOrder"); - assert_eq!(orders_errors.codes[1].code, 2); - - let order_item = &orders.structs[0]; - assert_eq!(order_item.name, "OrderItem"); - assert_eq!(order_item.fields.len(), 3); - - let order = &orders.structs[1]; - assert_eq!(order.name, "Order"); - assert_eq!(order.fields.len(), 4); - assert_eq!( - order.fields[1].ty, - TypeRef::List(Box::new(TypeRef::Named("OrderItem".to_string()))) - ); - - assert_eq!(orders.functions[0].name, "create_order"); - assert_eq!(orders.functions[0].returns, Some(TypeRef::I64)); - assert!(orders.functions[0].throws); - assert_eq!(orders.functions[1].name, "get_order"); - assert_eq!( - orders.functions[1].returns, - Some(TypeRef::Named("Order".to_string())) - ); - assert!(orders.functions[1].throws); - assert_eq!(orders.functions[2].name, "cancel_order"); - assert_eq!(orders.functions[2].returns, Some(TypeRef::Bool)); - assert!(!orders.functions[2].throws); - assert_eq!(orders.functions[3].name, "add_product_to_order"); - assert_eq!(orders.functions[3].returns, Some(TypeRef::Bool)); - assert_eq!(orders.functions[3].params.len(), 2); - assert_eq!(orders.functions[3].params[0].name, "order_id"); - assert_eq!(orders.functions[3].params[0].ty, TypeRef::I64); - assert_eq!(orders.functions[3].params[1].name, "product"); - assert_eq!( - orders.functions[3].params[1].ty, - TypeRef::Named("Product".to_string()) - ); - } - - #[test] - fn parse_async_demo_sample() { - let yaml = std::fs::read_to_string("../../samples/async-demo/async_demo.yml").unwrap(); - let api = parse_api_str(&yaml, "yml").unwrap(); - assert_eq!(api.version, "0.7.0"); - assert_eq!(api.modules.len(), 1); - - let m = &api.modules[0]; - assert_eq!(m.name, "tasks"); - - assert_eq!(m.structs.len(), 1); - let s = &m.structs[0]; - assert_eq!(s.name, "TaskResult"); - assert_eq!(s.fields.len(), 3); - assert_eq!(s.fields[0].name, "id"); - assert_eq!(s.fields[0].ty, TypeRef::I64); - assert_eq!(s.fields[1].name, "value"); - assert_eq!(s.fields[1].ty, TypeRef::StringUtf8); - assert_eq!(s.fields[2].name, "success"); - assert_eq!(s.fields[2].ty, TypeRef::Bool); - - assert_eq!(m.functions.len(), 5); - - let errors = m.errors.as_ref().expect("TaskError domain"); - assert_eq!(errors.name, "TaskError"); - assert_eq!(errors.codes.len(), 1); - assert_eq!(errors.codes[0].name, "InvalidName"); - assert_eq!(errors.codes[0].code, 1); - assert_eq!(errors.codes[0].message, "task name must not be empty"); - - let run_task = &m.functions[0]; - assert_eq!(run_task.name, "run_task"); - assert!(run_task.r#async); - assert!(run_task.throws); - assert_eq!( - run_task.returns, - Some(TypeRef::Named("TaskResult".to_string())) - ); - - let run_batch = &m.functions[1]; - assert_eq!(run_batch.name, "run_batch"); - assert!(run_batch.r#async); - assert_eq!( - run_batch.returns, - Some(TypeRef::List(Box::new(TypeRef::Named( - "TaskResult".to_string() - )))) - ); - - let cancel_task = &m.functions[2]; - assert_eq!(cancel_task.name, "cancel_task"); - assert!(!cancel_task.r#async); - assert_eq!(cancel_task.returns, Some(TypeRef::Bool)); - - let run_n_tasks = &m.functions[3]; - assert_eq!(run_n_tasks.name, "run_n_tasks"); - assert!(run_n_tasks.r#async); - assert_eq!(run_n_tasks.returns, Some(TypeRef::I32)); - - let active_callbacks = &m.functions[4]; - assert_eq!(active_callbacks.name, "active_callbacks"); - assert!(!active_callbacks.r#async); - assert_eq!(active_callbacks.returns, Some(TypeRef::I64)); - } - - #[test] - fn parse_idl_doc_map_example() { - let yaml = r#" -version: "0.7.0" -modules: - - name: example - structs: - - name: Contact - fields: - - { name: id, type: i64 } - - { name: name, type: string } - - { name: email, type: "string?" } - functions: - - name: update_scores - params: - - { name: scores, type: "{string:i32}" } - return: bool - - - name: get_contacts - params: [] - return: "{string:Contact}" - - - name: merge_tags - params: - - { name: current, type: "{string:string}" } - - { name: additions, type: "{string:string}" } - return: "{string:string}" -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - let m = &api.modules[0]; - assert_eq!(m.functions.len(), 3); - - let f0 = &m.functions[0]; - assert_eq!(f0.name, "update_scores"); - assert_eq!( - f0.params[0].ty, - TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32)) - ); - assert_eq!(f0.returns, Some(TypeRef::Bool)); - - let f1 = &m.functions[1]; - assert_eq!(f1.name, "get_contacts"); - assert_eq!( - f1.returns, - Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::Named("Contact".into())) - )) - ); - - let f2 = &m.functions[2]; - assert_eq!(f2.name, "merge_tags"); - assert_eq!( - f2.params[0].ty, - TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::StringUtf8)) - ); - assert_eq!( - f2.returns, - Some(TypeRef::Map( - Box::new(TypeRef::StringUtf8), - Box::new(TypeRef::StringUtf8) - )) - ); - } - - #[test] - fn parse_nested_modules() { - let yaml = r#" -version: "0.7.0" -modules: - - name: parent - functions: - - name: top_fn - params: [] - return: i32 - modules: - - name: child - functions: - - name: inner_fn - params: - - name: x - type: i32 - return: i32 -"#; - let api = parse_api_str(yaml, "yaml").unwrap(); - assert_eq!(api.modules.len(), 1); - let parent = &api.modules[0]; - assert_eq!(parent.name, "parent"); - assert_eq!(parent.functions.len(), 1); - assert_eq!(parent.functions[0].name, "top_fn"); - - assert_eq!(parent.modules.len(), 1); - let child = &parent.modules[0]; - assert_eq!(child.name, "child"); - assert_eq!(child.functions.len(), 1); - assert_eq!(child.functions[0].name, "inner_fn"); - assert_eq!(child.functions[0].params[0].name, "x"); - assert_eq!(child.functions[0].params[0].ty, TypeRef::I32); - assert_eq!(child.functions[0].returns, Some(TypeRef::I32)); + fn unknown_type_syntax_is_a_parse_error() { + let yaml = "version: \"0.8.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-macros/src/codegen/async_fns.rs b/crates/weaveffi-macros/src/codegen/async_fns.rs index 5d8f2863..727c816a 100644 --- a/crates/weaveffi-macros/src/codegen/async_fns.rs +++ b/crates/weaveffi-macros/src/codegen/async_fns.rs @@ -16,8 +16,8 @@ use proc_macro2::TokenStream; use quote::quote; +use weaveffi_core::model::Ty; use weaveffi_core::model::{AsyncBinding, FnBinding, ParamBinding}; -use weaveffi_ir::ir::TypeRef; use super::helpers::{ ctype_to_rust, fn_slots, ident, is_copy, sentinel, user_param_type, CallTarget, @@ -46,7 +46,7 @@ fn lift_async_input( // caller's thread, decode on the worker thread (inside its catch_unwind, // so a malformed buffer is delivered through the callback's `err` with // the panic code rather than unwinding across the C boundary). - if weaveffi_core::abi::is_buffered(&pb.ty) { + if pb.ty.is_buffered() { let ptr = ident(&format!("{}_ptr", pb.name)); let len = ident(&format!("{}_len", pb.name)); let panic_msg = format!("{}: malformed WeaveFFI value buffer", pb.name); @@ -65,12 +65,12 @@ fn lift_async_input( )); } Ok(match &pb.ty { - ty if is_copy(ty) && !matches!(ty, TypeRef::Enum(_)) => (none.clone(), none, quote!(#name)), + ty if is_copy(ty) && !matches!(ty, Ty::Enum(_)) => (none.clone(), none, quote!(#name)), // A typed handle is an opaque pointer, which is not `Send`. Carry it // across the worker-thread boundary as a `usize` and rebuild it inside // the closure with the producer's own pointer type (so a `super::T` // path stays in scope), mirroring the hand-written async sample. - TypeRef::TypedHandle(_) => { + Ty::TypedHandle(_) => { let addr = ident(&format!("__wv_addr_{}", pb.name)); let uty = user_param_type(sig, &pb.name) .ok_or_else(|| unsupported(&pb.name, "async typed-handle parameter"))?; @@ -80,17 +80,17 @@ fn lift_async_input( quote!(#name), ) } - TypeRef::StringUtf8 => ( + Ty::StringUtf8 => ( quote!(let #name = ::weaveffi::abi::c_ptr_to_string(#name).unwrap_or_default();), none, quote!(#name), ), - TypeRef::BorrowedStr => ( + Ty::BorrowedStr => ( quote!(let #name = ::weaveffi::abi::c_ptr_to_string(#name).unwrap_or_default();), none, quote!(&#name), ), - TypeRef::Bytes => { + Ty::Bytes => { let ptr = ident(&format!("{}_ptr", pb.name)); let len = ident(&format!("{}_len", pb.name)); ( @@ -99,7 +99,7 @@ fn lift_async_input( quote!(#name), ) } - TypeRef::BorrowedBytes => { + Ty::BorrowedBytes => { let ptr = ident(&format!("{}_ptr", pb.name)); let len = ident(&format!("{}_len", pb.name)); ( @@ -121,14 +121,11 @@ fn lift_async_input( /// and an owned interface result is adopted (the consumer eventually calls /// `_destroy`). Ownership transfer is what lets consumers defer decoding past /// the callback's return. `bytes` results are not yet supported. -fn async_result_args( - ty: &TypeRef, - value: TokenStream, -) -> syn::Result<(TokenStream, Vec)> { +fn async_result_args(ty: &Ty, value: TokenStream) -> syn::Result<(TokenStream, Vec)> { let none = TokenStream::new(); // A buffered result crosses as an owned `(ptr, len)` pair the consumer // decodes at its leisure and releases with `weaveffi_free_bytes`. - if weaveffi_core::abi::is_buffered(ty) { + if ty.is_buffered() { return Ok(( quote! { let __wv_res_buf = ::weaveffi::abi::encode_value(&(#value)).into_boxed_slice(); @@ -139,16 +136,16 @@ fn async_result_args( )); } Ok(match ty { - t if is_copy(t) && !matches!(t, TypeRef::Enum(_)) => (none.clone(), vec![value]), - TypeRef::Enum(_) => (none.clone(), vec![quote!((#value) as i32)]), + t if is_copy(t) && !matches!(t, Ty::Enum(_)) => (none.clone(), vec![value]), + Ty::Enum(_) => (none.clone(), vec![quote!((#value) as i32)]), // An owned string the consumer releases with `weaveffi_free_string`. - TypeRef::StringUtf8 | TypeRef::BorrowedStr => ( + Ty::StringUtf8 | Ty::BorrowedStr => ( quote!(let __wv_res = ::weaveffi::abi::string_to_c_ptr(&(#value));), vec![quote!(__wv_res)], ), // An owned-object result: the callback adopts the pointer (the plan's // `result_adopt`) and the consumer eventually calls `_destroy`. - TypeRef::Interface(_) => ( + Ty::Interface(_) => ( none.clone(), vec![quote!(::std::boxed::Box::into_raw(::std::boxed::Box::new(#value)))], ), diff --git a/crates/weaveffi-macros/src/codegen/callbacks.rs b/crates/weaveffi-macros/src/codegen/callbacks.rs index 86fb53c5..dbaffc6a 100644 --- a/crates/weaveffi-macros/src/codegen/callbacks.rs +++ b/crates/weaveffi-macros/src/codegen/callbacks.rs @@ -4,8 +4,8 @@ use proc_macro2::{Span, TokenStream}; use quote::quote; +use weaveffi_core::model::Ty; use weaveffi_core::model::{CallbackBinding, ListenerBinding, ModuleBinding, ParamBinding}; -use weaveffi_ir::ir::TypeRef; use super::helpers::{ctype_to_rust, ident, is_copy, rust_type_ident, typeref_to_rust}; use super::unsupported; @@ -40,7 +40,7 @@ fn emit_callback_param( // A buffered payload is borrowed for the dispatch: encode it once into a // local value buffer, hand every subscriber the same `(ptr, len)` view, // and let the encoding drop after the loop. - if weaveffi_core::abi::is_buffered(&pb.ty) { + if pb.ty.is_buffered() { let rt = typeref_to_rust(&pb.ty)?; let tmp = ident(&format!("__wv_cb_{}", pb.name)); return Ok(( @@ -51,7 +51,7 @@ fn emit_callback_param( )); } Ok(match &pb.ty { - TypeRef::Enum(name) => { + Ty::Enum(name) => { let et = rust_type_ident(name); ( quote!(#n: #et), @@ -64,7 +64,7 @@ fn emit_callback_param( let rt = typeref_to_rust(ty)?; (quote!(#n: #rt), none.clone(), vec![quote!(#n)], none) } - TypeRef::StringUtf8 | TypeRef::BorrowedStr => { + Ty::StringUtf8 | Ty::BorrowedStr => { let tmp = ident(&format!("__wv_cb_{}", pb.name)); ( quote!(#n: &str), @@ -73,7 +73,7 @@ fn emit_callback_param( quote!(::weaveffi::abi::free_string(#tmp);), ) } - TypeRef::Bytes | TypeRef::BorrowedBytes => ( + Ty::Bytes | Ty::BorrowedBytes => ( quote!(#n: &[u8]), none.clone(), vec![quote!(#n.as_ptr()), quote!(#n.len())], diff --git a/crates/weaveffi-macros/src/codegen/helpers.rs b/crates/weaveffi-macros/src/codegen/helpers.rs index 54528e93..415efd93 100644 --- a/crates/weaveffi-macros/src/codegen/helpers.rs +++ b/crates/weaveffi-macros/src/codegen/helpers.rs @@ -6,7 +6,7 @@ use quote::quote; use syn::Ident; use weaveffi_core::abi::{AbiParam, CType, ConstPos}; use weaveffi_core::model::FnBinding; -use weaveffi_ir::ir::TypeRef; +use weaveffi_core::model::Ty; use super::{unsupported, PREFIX}; @@ -97,22 +97,22 @@ pub(crate) fn sentinel(ret: &CType) -> TokenStream { /// True when this enum/struct field/param type crosses the ABI without owning /// heap data (so a getter can read it by copy rather than clone). -pub(crate) fn is_copy(ty: &TypeRef) -> bool { +pub(crate) fn is_copy(ty: &Ty) -> bool { matches!( ty, - TypeRef::I8 - | TypeRef::I16 - | TypeRef::I32 - | TypeRef::I64 - | TypeRef::U8 - | TypeRef::U16 - | TypeRef::U32 - | TypeRef::U64 - | TypeRef::F32 - | TypeRef::F64 - | TypeRef::Bool - | TypeRef::Handle - | TypeRef::Enum(_) + Ty::I8 + | Ty::I16 + | Ty::I32 + | Ty::I64 + | Ty::U8 + | Ty::U16 + | Ty::U32 + | Ty::U64 + | Ty::F32 + | Ty::F64 + | Ty::Bool + | Ty::Handle + | Ty::Enum(_) ) } @@ -176,7 +176,7 @@ fn typed_handle_user_type<'a>( sig: &'a syn::Signature, ) -> Option<&'a syn::Type> { let pb = f.params.iter().find(|pb| pb.name == p.name)?; - if !matches!(pb.ty, TypeRef::TypedHandle(_)) { + if !matches!(pb.ty, Ty::TypedHandle(_)) { return None; } user_param_type(sig, &p.name) @@ -185,7 +185,7 @@ fn typed_handle_user_type<'a>( /// Render the `-> T` return clause, preferring the producer's own type for a /// typed-handle return (so a parent-module `super::T` stays in scope). pub(crate) fn ret_arrow_for(ret: &CType, f: &FnBinding, sig: &syn::Signature) -> TokenStream { - if matches!(f.ret, Some(TypeRef::TypedHandle(_))) { + if matches!(f.ret, Some(Ty::TypedHandle(_))) { if let syn::ReturnType::Type(_, ty) = &sig.output { let ty = weaveffi_bridge::peel_result(ty); return quote!(-> #ty); @@ -281,44 +281,43 @@ pub(crate) fn wrap_unwind(body: TokenStream, sentinel: Option<&TokenStream>) -> /// Spell an IR type as the Rust type a producer uses, for the cases the macro /// needs to name a generic argument (currently the element of `iter`). The /// map flavor is not recorded by the IR, so it defaults to `HashMap`. -pub(crate) fn typeref_to_rust(ty: &TypeRef) -> syn::Result { +pub(crate) fn typeref_to_rust(ty: &Ty) -> syn::Result { Ok(match ty { - TypeRef::I8 => quote!(i8), - TypeRef::I16 => quote!(i16), - TypeRef::I32 => quote!(i32), - TypeRef::I64 => quote!(i64), - TypeRef::U8 => quote!(u8), - TypeRef::U16 => quote!(u16), - TypeRef::U32 => quote!(u32), - TypeRef::U64 => quote!(u64), - TypeRef::F32 => quote!(f32), - TypeRef::F64 => quote!(f64), - TypeRef::Bool => quote!(bool), - TypeRef::Handle => quote!(u64), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => quote!(::std::string::String), - TypeRef::Bytes | TypeRef::BorrowedBytes => quote!(::std::vec::Vec), - TypeRef::Record(s) | TypeRef::RichEnum(s) | TypeRef::Enum(s) | TypeRef::Interface(s) => { + Ty::I8 => quote!(i8), + Ty::I16 => quote!(i16), + Ty::I32 => quote!(i32), + Ty::I64 => quote!(i64), + Ty::U8 => quote!(u8), + Ty::U16 => quote!(u16), + Ty::U32 => quote!(u32), + Ty::U64 => quote!(u64), + Ty::F32 => quote!(f32), + Ty::F64 => quote!(f64), + Ty::Bool => quote!(bool), + Ty::Handle => quote!(u64), + Ty::StringUtf8 | Ty::BorrowedStr => quote!(::std::string::String), + Ty::Bytes | Ty::BorrowedBytes => quote!(::std::vec::Vec), + Ty::Record(s) | Ty::RichEnum(s) | Ty::Enum(s) | Ty::Interface(s) => { let ty = rust_type_ident(s); quote!(#ty) } - TypeRef::Named(n) => unreachable!("unresolved type reference '{n}'"), - TypeRef::TypedHandle(n) => { + Ty::TypedHandle(n) => { let ty = rust_type_ident(n); quote!(*mut #ty) } - TypeRef::Optional(inner) => { + Ty::Optional(inner) => { let inner = typeref_to_rust(inner)?; quote!(::std::option::Option<#inner>) } - TypeRef::List(inner) => { + Ty::List(inner) => { let inner = typeref_to_rust(inner)?; quote!(::std::vec::Vec<#inner>) } - TypeRef::Map(k, v) => { + Ty::Map(k, v) => { let k = typeref_to_rust(k)?; let v = typeref_to_rust(v)?; quote!(::std::collections::HashMap<#k, #v>) } - TypeRef::Iterator(_) => return Err(unsupported("type", "nested iterator")), + Ty::Iterator(_) => return Err(unsupported("type", "nested iterator")), }) } diff --git a/crates/weaveffi-macros/src/codegen/marshal.rs b/crates/weaveffi-macros/src/codegen/marshal.rs index 99b48d01..957a5689 100644 --- a/crates/weaveffi-macros/src/codegen/marshal.rs +++ b/crates/weaveffi-macros/src/codegen/marshal.rs @@ -15,9 +15,9 @@ use proc_macro2::TokenStream; use quote::quote; -use weaveffi_core::abi::{is_buffered, CType}; +use weaveffi_core::abi::CType; +use weaveffi_core::model::Ty; use weaveffi_core::model::{FieldBinding, ParamBinding}; -use weaveffi_ir::ir::TypeRef; use super::helpers::{ident, is_copy, rust_type_ident, sentinel}; use super::unsupported; @@ -55,7 +55,7 @@ pub(crate) fn lift_param( // call site. A malformed buffer is a producer/consumer contract // violation, reported through `out_err` with the reserved marshalling // code so it can't shadow a domain's typed codes. - if is_buffered(&pb.ty) { + if pb.ty.is_buffered() { let ptr = ident(&format!("{}_ptr", pb.name)); let len = ident(&format!("{}_len", pb.name)); let slice = buffer_slice_expr(&ptr, &len); @@ -81,8 +81,8 @@ pub(crate) fn lift_param( } Ok(match &pb.ty { - ty if is_copy(ty) && !matches!(ty, TypeRef::Enum(_)) => (TokenStream::new(), owned()), - TypeRef::Enum(enum_name) => { + ty if is_copy(ty) && !matches!(ty, Ty::Enum(_)) => (TokenStream::new(), owned()), + Ty::Enum(enum_name) => { let et = rust_type_ident(enum_name); let pre = quote! { let #name = match #et::__weaveffi_from_i32(#name) { @@ -99,7 +99,7 @@ pub(crate) fn lift_param( }; (pre, owned()) } - TypeRef::StringUtf8 => { + Ty::StringUtf8 => { let pre = quote! { let #name = match ::weaveffi::abi::c_ptr_to_string(#name) { ::std::option::Option::Some(__s) => __s, @@ -118,7 +118,7 @@ pub(crate) fn lift_param( // A borrowed string is lifted zero-copy: the thunk borrows the // caller's NUL-terminated buffer for the duration of the call, which // is the whole point of a producer taking `&str` over `String`. - TypeRef::BorrowedStr => { + Ty::BorrowedStr => { let pre = quote! { // SAFETY: the C contract guarantees the argument is null or a // NUL-terminated string valid for the duration of the call. @@ -136,7 +136,7 @@ pub(crate) fn lift_param( }; (pre, owned()) } - TypeRef::Bytes => { + Ty::Bytes => { let ptr = ident(&format!("{}_ptr", pb.name)); let len = ident(&format!("{}_len", pb.name)); ( @@ -144,7 +144,7 @@ pub(crate) fn lift_param( owned(), ) } - TypeRef::BorrowedBytes => { + Ty::BorrowedBytes => { let ptr = ident(&format!("{}_ptr", pb.name)); let len = ident(&format!("{}_len", pb.name)); ( @@ -154,7 +154,7 @@ pub(crate) fn lift_param( } // An interface parameter borrows the caller-owned object for the call; // the slot is a `const {tag}*`, so the producer must accept `&T`. - TypeRef::Interface(_) => { + Ty::Interface(_) => { if !is_ref { return Err(unsupported( &pb.name, @@ -175,11 +175,11 @@ pub(crate) fn lift_param( }; (pre, owned()) } - TypeRef::TypedHandle(_) => (TokenStream::new(), owned()), + Ty::TypedHandle(_) => (TokenStream::new(), owned()), // Only `Interface?` is optional and unbuffered; the macro does not // marshal it yet. - TypeRef::Optional(_) => return Err(unsupported(&pb.name, "optional interface parameter")), - TypeRef::Iterator(_) => return Err(unsupported(&pb.name, "iterator parameter")), + Ty::Optional(_) => return Err(unsupported(&pb.name, "optional interface parameter")), + Ty::Iterator(_) => return Err(unsupported(&pb.name, "iterator parameter")), _ => return Err(unsupported(&pb.name, "parameter type")), }) } @@ -191,10 +191,10 @@ pub(crate) fn lift_param( /// [`weaveffi_core::plan::return_free`]: strings are released with /// `{prefix}_free_string`, byte and value buffers with `{prefix}_free_bytes`, /// and owned interface pointers with the type's `_destroy` symbol. -pub(crate) fn lower_value(ty: &TypeRef, value: TokenStream) -> syn::Result { +pub(crate) fn lower_value(ty: &Ty, value: TokenStream) -> syn::Result { // A buffered return is encoded into a producer-allocated value buffer and // returned exactly like a bytes return: base pointer plus `*out_len`. - if is_buffered(ty) { + if ty.is_buffered() { return Ok(quote! { unsafe { ::weaveffi::abi::lower_bytes( @@ -205,25 +205,25 @@ pub(crate) fn lower_value(ty: &TypeRef, value: TokenStream) -> syn::Result value, - TypeRef::Enum(_) => quote!((#value) as i32), - TypeRef::StringUtf8 | TypeRef::BorrowedStr => { + t if is_copy(t) && !matches!(t, Ty::Enum(_)) => value, + Ty::Enum(_) => quote!((#value) as i32), + Ty::StringUtf8 | Ty::BorrowedStr => { quote!(::weaveffi::abi::string_to_c_ptr(&(#value))) } - TypeRef::Bytes | TypeRef::BorrowedBytes => { + Ty::Bytes | Ty::BorrowedBytes => { quote!(unsafe { ::weaveffi::abi::lower_bytes(#value, out_len) }) } // A returned interface moves to the heap; the caller owns the new // reference and releases it with the type's `_destroy` symbol // (`ReturnFree::OwnedObject` in the plan). - TypeRef::Interface(_) => { + Ty::Interface(_) => { quote!(::std::boxed::Box::into_raw(::std::boxed::Box::new(#value))) } - TypeRef::TypedHandle(_) => value, + Ty::TypedHandle(_) => value, // Only `Interface?` is optional and unbuffered; the macro does not // marshal it yet. - TypeRef::Optional(_) => return Err(unsupported("return", "optional interface return")), - TypeRef::Iterator(_) => return Err(unsupported("return", "iterator return")), + Ty::Optional(_) => return Err(unsupported("return", "optional interface return")), + Ty::Iterator(_) => return Err(unsupported("return", "iterator return")), _ => return Err(unsupported("return", "return type")), }) } @@ -237,7 +237,7 @@ pub(crate) fn lower_value(ty: &TypeRef, value: TokenStream) -> syn::Result, + ret_ty: &Option, ret_ctype: &CType, is_throws: bool, call: TokenStream, @@ -300,7 +300,7 @@ pub(crate) fn build_call_body( /// exception is a typed handle, whose pointer identity crosses as a `u64`. pub(crate) fn field_write_stmt(field: &FieldBinding, access: TokenStream) -> TokenStream { match &field.ty { - TypeRef::TypedHandle(_) => quote!(__wv_w.write_u64(#access as u64);), + Ty::TypedHandle(_) => quote!(__wv_w.write_u64(#access as u64);), _ => quote!(::weaveffi::abi::BufferValue::write_value(&#access, __wv_w);), } } @@ -312,7 +312,7 @@ pub(crate) fn field_write_stmt(field: &FieldBinding, access: TokenStream) -> Tok /// what decoding targets. pub(crate) fn field_read_expr(field: &FieldBinding) -> TokenStream { match &field.ty { - TypeRef::TypedHandle(_) => quote!(__wv_r.read_u64().map(|__v| __v as _)), + Ty::TypedHandle(_) => quote!(__wv_r.read_u64().map(|__v| __v as _)), _ => quote!(::weaveffi::abi::BufferValue::read_value(__wv_r)), } } @@ -322,7 +322,7 @@ pub(crate) fn field_read_expr(field: &FieldBinding) -> TokenStream { /// [`field_write_stmt`] with reference access. pub(crate) fn field_write_stmt_ref(field: &FieldBinding, binding: &syn::Ident) -> TokenStream { match &field.ty { - TypeRef::TypedHandle(_) => quote!(__wv_w.write_u64(*#binding as u64);), + Ty::TypedHandle(_) => quote!(__wv_w.write_u64(*#binding as u64);), _ => quote!(::weaveffi::abi::BufferValue::write_value(#binding, __wv_w);), } } diff --git a/crates/weaveffi-macros/src/codegen/mod.rs b/crates/weaveffi-macros/src/codegen/mod.rs index 3aaff480..e4a69a6a 100644 --- a/crates/weaveffi-macros/src/codegen/mod.rs +++ b/crates/weaveffi-macros/src/codegen/mod.rs @@ -31,7 +31,7 @@ use proc_macro2::{Span, TokenStream}; use quote::quote; use weaveffi_core::model::{BindingModel, ErrorBinding, ModuleBinding}; use weaveffi_core::plan::ErrorStrategy; -use weaveffi_ir::ir::{Api, Module, TypeRef, CURRENT_SCHEMA_VERSION}; +use weaveffi_ir::ir::{Api, CURRENT_SCHEMA_VERSION}; use self::helpers::{ident, CallTarget}; use self::sync::gen_function; @@ -50,35 +50,25 @@ pub fn expand_module(item_mod: &syn::ItemMod) -> syn::Result { )); } - // 1. Lower to IR through the shared bridge, then resolve type references so - // C-style enum references are distinguished from record references and - // cross-module references are qualified. The bridge recurses into nested - // `#[weaveffi::module]` submodules, so this single lowering covers the - // whole module tree. + // 1. Lower to IR through the shared bridge. The bridge recurses into + // nested `#[weaveffi::module]` submodules, so this single lowering + // covers the whole module tree. // - // We run `resolve_type_refs` rather than the full `validate_api`: a - // `#[weaveffi::module]` is expanded in isolation, so a reference to a type - // declared in a *sibling* top-level module (e.g. `orders` using a - // `products::Product`) must not be rejected as an unknown type here. A - // cross-module struct still crosses the ABI as a value buffer, and the - // emitted thunk decodes into the producer's real Rust type, so the symbol - // and calling convention match the header regardless. Whole-API rule - // checks remain enforced by the CLI's `validate`/`extract`/`generate`, - // and the Rust compiler rejects any genuinely undefined type in the - // thunks. + // We skip the full `validate_api`: a `#[weaveffi::module]` is expanded + // in isolation, so a reference to a type declared in a *sibling* + // top-level module (e.g. `orders` using a `products::Product`) must not + // be rejected as an unknown type here. `ResolvedApi::assume_valid` + // resolves such names to `Ty::Record`, which is the right marshalling + // for either a record or a rich enum (both cross as value buffers), and + // the emitted thunk decodes into the producer's real Rust type, so the + // symbol and calling convention match the header regardless. Whole-API + // rule checks remain enforced by the CLI's `validate`/`generate`, and + // the Rust compiler rejects any genuinely undefined type in the thunks. let module_ir = weaveffi_bridge::module_from_item_mod(item_mod)?; - let mut api = Api { + let api = weaveffi_core::ResolvedApi::assume_valid(Api { version: CURRENT_SCHEMA_VERSION.to_string(), - package: None, modules: vec![module_ir], - generators: None, - }; - weaveffi_core::validate::resolve_type_refs(&mut api); - resolve_sibling_named_refs(&mut api); - // After `resolve_type_refs` plus the sibling rewrite above, no `Named` - // reference remains, which is exactly the contract `assume_resolved` - // asserts (see the doc on `resolve_sibling_named_refs`). - let api = weaveffi_core::ResolvedApi::assume_resolved(api); + }); // 2. Build the canonical lowered model. Nested modules are flattened into // one binding each, keyed here by their path segments (e.g. `["kv", @@ -98,79 +88,6 @@ pub fn expand_module(item_mod: &syn::ItemMod) -> syn::Result { rebuild_module(item_mod, &[], &by_path) } -/// Rewrite every [`TypeRef::Named`] left after resolution into -/// [`TypeRef::Record`]. -/// -/// The resolver only rewrites names it finds in the expanded module tree, and -/// the macro expands each `#[weaveffi::module]` in isolation, so a reference -/// to a type declared in a sibling top-level module stays `Named` here (the -/// CLI, which sees the whole API, always resolves it). Such a reference -/// crosses the ABI as a value buffer regardless of whether the sibling -/// declares a record or a rich enum - both are buffered value types - so -/// `Record` marshalling is correct for both, and the core's ABI lowering -/// would otherwise panic on a leftover `Named`. A genuinely undefined type -/// still fails: rustc rejects the thunk that names it. -fn resolve_sibling_named_refs(api: &mut Api) { - fn walk_type(ty: &mut TypeRef) { - match ty { - TypeRef::Named(name) => { - let name = std::mem::take(name); - *ty = TypeRef::Record(name); - } - TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => { - walk_type(inner); - } - TypeRef::Map(k, v) => { - walk_type(k); - walk_type(v); - } - _ => {} - } - } - fn walk_module(module: &mut Module) { - let callables = module - .functions - .iter_mut() - .chain(module.interfaces.iter_mut().flat_map(|i| { - i.constructors - .iter_mut() - .chain(i.methods.iter_mut()) - .chain(i.statics.iter_mut()) - })); - for f in callables { - for p in &mut f.params { - walk_type(&mut p.ty); - } - if let Some(ret) = &mut f.returns { - walk_type(ret); - } - } - for s in &mut module.structs { - for field in &mut s.fields { - walk_type(&mut field.ty); - } - } - for e in &mut module.enums { - for v in &mut e.variants { - for field in &mut v.fields { - walk_type(&mut field.ty); - } - } - } - for cb in &mut module.callbacks { - for p in &mut cb.params { - walk_type(&mut p.ty); - } - } - for child in &mut module.modules { - walk_module(child); - } - } - for module in &mut api.modules { - walk_module(module); - } -} - /// Re-emit `item_mod` with its generated thunks appended, recursing into nested /// `#[weaveffi::module]` submodules. fn rebuild_module( diff --git a/crates/weaveffi/Cargo.toml b/crates/weaveffi/Cargo.toml index 73fbcf68..7ab56cc1 100644 --- a/crates/weaveffi/Cargo.toml +++ b/crates/weaveffi/Cargo.toml @@ -13,5 +13,9 @@ categories = ["development-tools::ffi"] weaveffi-abi = { path = "../weaveffi-abi", version = "0.21.0" } weaveffi-macros = { path = "../weaveffi-macros", version = "0.21.0" } +[dev-dependencies] +# Only for the ABI-revision lockstep test; the facade never links core at runtime. +weaveffi-core = { path = "../weaveffi-core", version = "0.21.0" } + [lints] workspace = true diff --git a/crates/weaveffi/tests/abi_version.rs b/crates/weaveffi/tests/abi_version.rs new file mode 100644 index 00000000..34851362 --- /dev/null +++ b/crates/weaveffi/tests/abi_version.rs @@ -0,0 +1,23 @@ +//! The ABI revision is declared twice on purpose: once in `weaveffi-abi` +//! (which producers link) and once in `weaveffi-core` (which generators +//! read). The runtime crate must not depend on the generator stack, so +//! this test is what keeps the two numbers equal. + +#![allow(unsafe_code)] + +weaveffi::export_runtime!(); + +#[test] +fn runtime_and_generator_abi_revisions_agree() { + assert_eq!(weaveffi::abi::ABI_VERSION, weaveffi_core::cabi::ABI_VERSION); +} + +#[test] +fn exported_thunk_reports_the_runtime_revision() { + assert_eq!(weaveffi_abi_version(), weaveffi::abi::ABI_VERSION); +} + +#[test] +fn header_alias_table_covers_the_version_symbol() { + assert!(weaveffi_core::utils::ABI_RUNTIME_SYMBOLS.contains(&"abi_version")); +} diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 9ee1360e..f2964a3e 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -35,7 +35,7 @@ - [Error Handling](guides/errors.md) - [Async Functions](guides/async.md) - [Annotated Rust Extraction](guides/extract.md) - - [Generator Configuration](guides/config.md) + - [Project Configuration](guides/config.md) - [Packaging and Distribution](guides/packaging.md) - [Tutorials](tutorials/README.md) - [Calculator](tutorials/calculator.md) diff --git a/docs/src/architecture.md b/docs/src/architecture.md index 21bb2dd3..4b49f1f4 100644 --- a/docs/src/architecture.md +++ b/docs/src/architecture.md @@ -18,23 +18,24 @@ Parse ── weaveffi-ir::parse (IDL) | weaveffi-bridge (.rs): builds an │ ▼ Validate ── weaveffi-core::validate: rejects errors, collects warnings, - │ rewrites every parsed type reference to its resolved kind, - │ and wraps the result in a `ResolvedApi` proof type + │ indexes every user-type declaration, and wraps the untouched + │ document in a `ResolvedApi` proof type │ ▼ -Resolve ── weaveffi-cli `CliConfig`: merges --config TOML and the - │ inline generators: section into each target's typed config +Configure ── weaveffi-cli `ProjectConfig`: loads the nearest weaveffi.toml + │ ([package], [global], [generators.]) and attaches the + │ package identity to the `ResolvedApi` ▼ Generate ── weaveffi-core::codegen::Orchestrator: dispatches every - │ selected target generator in parallel via rayon + │ selected target in parallel via rayon; each target builds a + │ `BindingModel` (every type a resolved `Ty`) and renders it ▼ -Output ── Each generator writes its files under {out_dir}/{target}/ +Output ── Each target writes its files under {out_dir}/{target}/ and updates {out_dir}/.weaveffi-cache/{target}.hash ``` -Subcommands like `validate`, `lint`, `diff`, `format`, and `watch` re-use -the parse and validate stages; `generate`, `diff`, and `watch` -additionally exercise resolve and generate. +`validate` stops after the second stage; `generate`, `diff`, and `package` +run all of them. A `.rs` input is lowered to the IR by `weaveffi-bridge`, the same extractor the `#[weaveffi::module]` proc-macro uses to build a producer's C ABI glue. @@ -77,19 +78,18 @@ weaveffi-fuzz ──► weaveffi-ir, weaveffi-core (workspace-private; unpublish | Crate | What it owns | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| `weaveffi-ir` | The IR types (`Api`, `Module`, `Function`, `TypeRef`, …), the `parse_api_str` parser, the `parse_type_ref` mini-grammar, and `CURRENT_SCHEMA_VERSION`. | -| `weaveffi-abi` | Stable C ABI runtime symbols: `weaveffi_error`, `weaveffi_error_clear`, `weaveffi_free_string`, `weaveffi_free_bytes`, the arena, cancel tokens, the `lift_*`/`lower_*` marshalling converters the macro calls, and the `export_runtime!` macro. | +| `weaveffi-ir` | The IR types (`Api`, `Module`, `Function`, `TypeRef`, …), the `parse_api_str` parser, the `parse_type_ref` mini-grammar, and `CURRENT_SCHEMA_VERSION`. The document model only: no package or generator configuration. | +| `weaveffi-abi` | Stable C ABI runtime symbols: `weaveffi_abi_version`, `weaveffi_error`, `weaveffi_error_clear`, `weaveffi_free_string`, `weaveffi_free_bytes`, the arena, cancel tokens, the `lift_*`/`lower_*` marshalling converters the macro calls, and the `export_runtime!` macro. | | `weaveffi-bridge` | The single Rust-to-IR extractor: maps `#[weaveffi::module]`-annotated source (`syn` AST) to an `Api`. Shared by the proc-macro and the CLI's `extract`/`generate `. | | `weaveffi-macros` | The `#[weaveffi::module]` proc-macro family. Lowers an annotated module through `weaveffi-bridge`, builds the `BindingModel`, and emits the `#[no_mangle] extern "C"` thunks (marshalling via `weaveffi-abi`). Emission is decomposed into nine focused submodules under `src/codegen/` (sync calls, async launchers, iterators, records, enums, interfaces, callbacks, marshalling, and shared helpers), each stating which clause of the `weaveffi_core::plan` contracts it implements. | | `weaveffi` | The producer facade a Rust cdylib depends on: re-exports the `weaveffi-macros` attributes, `export_runtime!`, and `weaveffi-abi` as `weaveffi::abi`. | -| `weaveffi-core` | The `Generator` trait, the `LanguageBackend` framework + driver, the `Orchestrator`, the `abi` C-ABI lowering model, the `plan` marshalling-plan module (the language-neutral calling contracts; see [The marshalling plan](#the-marshalling-plan)), the `wire` classification of buffer contents, the `lang` per-language keyword tables, the `manifest` escaping helpers, the `BindingModel`, validation rules, generator config resolution, and the per-generator hash cache. | -| `weaveffi-gen-*` | Eleven generator crates. Each implements `LanguageBackend` (bridged to `Generator` by `impl_generator_via_backend!`) and produces target-specific output (header, wrapper, package metadata). Each follows a shared internal layout; see [Generator crate layout](#generator-crate-layout). | -| `weaveffi-cli` | The `weaveffi` binary. Parses the IDL, applies validation, instantiates every generator (via the `cli_targets!` registry in `config.rs`), and dispatches the `Orchestrator`. Subcommands live under `commands/` (`generate`, `validate`, `diff`, `format`, `package`, `new`, `watch`); `doctor.rs`, `extract.rs`, and `scaffold.rs` sit beside `main.rs`; `config.rs` holds the target registry and config resolution; `report.rs` formats CLI output. | +| `weaveffi-core` | Validation and the `ResolvedApi` proof type, the resolved `Ty` type model with its `Family` and `WireType` classifications, the `BindingModel`, the `LanguageBackend` trait and its `Target`/`ConfiguredBackend` object-safe view, the `Orchestrator`, the `abi` C-ABI lowering model, the `cabi` shared C declaration renderer, the `plan` marshalling-plan module (the language-neutral calling contracts; see [The marshalling plan](#the-marshalling-plan)), the `pkg` package-identity policy, the `lang` per-language keyword tables, the `manifest` escaping helpers, and the per-target hash cache. | +| `weaveffi-gen-*` | Eleven generator crates. Each implements `LanguageBackend` and produces target-specific output (header, wrapper, package metadata). Each follows a shared internal layout; see [Generator crate layout](#generator-crate-layout). | +| `weaveffi-cli` | The `weaveffi` binary. Loads the definition and the project's `weaveffi.toml`, applies validation, instantiates every target (via the `cli_targets!` registry in `config.rs`), and dispatches the `Orchestrator`. Subcommands live under `commands/` (`generate`, `validate`, `diff`, `package`); `extract.rs` sits beside `main.rs`; `config.rs` holds the target registry and `ProjectConfig`; `report.rs` formats CLI output. | | `weaveffi-fuzz` | `cargo-fuzz` harnesses for the parsers, the validator, and `parse_type_ref`. Workspace-private (not published to crates.io). | Crates that contain `unsafe` code opt in explicitly: `weaveffi-abi`, -`weaveffi-fuzz`, the scaffold output emitted by `weaveffi generate --scaffold`, -and any `samples/*` producer that dereferences a raw handle pointer in its own +`weaveffi-fuzz`, and any `samples/*` producer that dereferences a raw handle pointer in its own helpers (such as `kvstore`) add `#![allow(unsafe_code)]` at the top of their main source file. The thunks the `#[weaveffi::module]` macro emits instead carry a scoped `#[allow(unsafe_code)]` on each generated function, so @@ -105,17 +105,15 @@ module: | Module | Responsibility | | ------------- | --------------------------------------------------------------------- | | `main.rs` | `clap` definitions and top-level dispatch into `commands/`. | -| `config.rs` | The `cli_targets!` registry macro, the generated `CliConfig`, and config resolution (`--config` TOML + inline `generators:`). | +| `config.rs` | The `cli_targets!` registry macro, the generated `Targets` struct, and `ProjectConfig` (`weaveffi.toml` discovery, parsing, and the `[global]` fan-outs). | | `report.rs` | Human-readable formatting of generate/diff results and summaries. | -| `commands/` | One module per subcommand: `generate`, `validate`, `diff`, `format`, `package`, `new`, `watch` (re-exported through `commands/mod.rs`). | -| `doctor.rs` | `weaveffi doctor`: probes host toolchains per target. | +| `commands/` | One module per subcommand: `generate`, `validate`, `diff`, `package` (re-exported through `commands/mod.rs`, alongside the shared `load_project` step every generating command starts with). | | `extract.rs` | `weaveffi extract`: a thin wrapper over `weaveffi-bridge` that serializes the derived IDL. | -| `scaffold.rs` | the Rust producer stubs emitted by `weaveffi generate --scaffold` (for non-macro producers). | #### The `cli_targets!` registry The 11 language targets used to be spelled out a dozen times (config -struct fields, the `--target` parser, inline-generator merging, and the +struct fields, the `--target` parser, config merging, and the `Orchestrator` wiring). They now live in **one** declarative macro, `cli_targets!`, defined and invoked in `config.rs`: @@ -125,74 +123,78 @@ cli_targets! { "cpp" => cpp: CppConfig via CppGenerator, "swift" => swift: SwiftConfig via SwiftGenerator, strip, // … one line per target … - "ruby" => ruby: RubyConfig via RubyGenerator, + "ruby" => ruby: RubyConfig via RubyGenerator, strip, } ``` -That single invocation expands to the `CliConfig` struct (one typed -field per target), `build_generators`, `apply_inline_target`, and the -`strip_module_prefix`/input-stamping fan-out. Adding a language is a -one-line change here; see [Adding a new generator](#adding-a-new-generator). - -#### Format canonicalization - -`weaveffi format` (and `format --check`) round-trips an IDL through the -IR and re-serializes it, so the on-disk form is *canonical*. For the -check to be a no-op on an already-formatted file, serialization must omit -every field that is at its default; otherwise `serde` would inject -`null`, `[]`, and `false` noise that the parser then drops on the next -read, making `format` non-idempotent. The IR types therefore tag their -optional/defaulted fields with `#[serde(skip_serializing_if = …)]` -(`Option::is_none`, `Vec::is_empty`, and a local `is_false` for booleans -that default to `false`). This keeps canonical IDLs terse and makes -`format` idempotent; it also removes the now-meaningless `default` -annotations from the generated `weaveffi.schema.json`. +That single invocation expands to the `Targets` struct (one typed field +per target, deserialized from the `[generators.]` tables), the +`build` method producing the ordered `Box` list, and the +`strip_module_prefix`/`c_prefix`/input-stamping fan-outs. Adding a +language is a one-line change here; see +[Adding a new generator](#adding-a-new-generator). + +#### Canonical serialization + +`weaveffi extract` serializes the derived IDL, so the IR types keep the +on-disk form *canonical*: every field at its default is omitted via +`#[serde(skip_serializing_if = …)]` (`Option::is_none`, `Vec::is_empty`, +and a local `is_false` for booleans that default to `false`). Without +that, `serde` would inject `null`, `[]`, and `false` noise that the parser +drops on the next read. It also keeps the generated `weaveffi.schema.json` +free of meaningless `default` annotations. ## The IR `weaveffi_ir::ir` defines a small algebraic type system. The shapes that matter most: -- `Api { version, modules, generators }`: root node. -- `Module { name, functions, interfaces, structs, enums, callbacks, +- `Api { version, modules }`: root node. Unknown top-level keys are + rejected (`deny_unknown_fields`), so a stale `package:` or + `generators:` block fails to parse instead of being silently dropped. +- `Module { name, doc, functions, interfaces, structs, enums, callbacks, listeners, errors, modules }`: modules can nest. - `Function { name, params, returns, doc, throws, async, cancellable, deprecated, since }`. The same shape describes an interface's constructors, methods, and statics. - `InterfaceDef { name, doc, constructors, methods, statics }`: an opaque object type; every interface also receives an implicit destroy symbol. -- `TypeRef` enumerates every supported type reference: primitives - (`I32`, `U32`, `I64`, `F64`, `Bool`, `StringUtf8`, `Bytes`, `Handle`, - `BorrowedStr`, `BorrowedBytes`), user types (`Named(String)`, - `Record(String)`, `RichEnum(String)`, `Enum(String)`, - `Interface(String)`, `TypedHandle(String)`), and the four composite +- `TypeRef` enumerates every type reference a document can *write*: + primitives (`I32`, `U32`, `I64`, `F64`, `Bool`, `StringUtf8`, `Bytes`, + `Handle`, `BorrowedStr`, `BorrowedBytes`), the two user-type spellings + (`Named(String)` for any record, enum, or interface by bare or + dot-qualified name, and `TypedHandle(String)`), and the four composite shapes (`Optional`, `List`, `Map`, `Iterator`). -User-type references are two-phase. The parser cannot know whether a -bare identifier names a record, an enum, or an interface, so every -user-type reference starts life as `TypeRef::Named`. The resolution -pass (`weaveffi_core::validate::resolve_type_refs`, run by -`validate_api` after the rule checks pass) rewrites each occurrence -into `Record` (a struct), `RichEnum` (an algebraic enum), `Enum` (a -C-style enum), or `Interface`, and qualifies cross-module references -with the owning module's dot-joined path. The post-resolution -guarantee is carried in the type system: -`weaveffi_core::resolved::ResolvedApi` wraps an `Api` that has passed -validation and resolution, and it is the only way to reach -`BindingModel::build`, the `LanguageBackend` trait, and the -`Orchestrator`. Generators therefore run strictly post-resolution: no -`Named` reference remains, and a generator may treat one as a bug (the -core ABI lowering panics on it). `Record` and -`RichEnum` are *buffered* types (`weaveffi_core::abi::is_buffered` -returns true for both, along with optionals, lists, and maps): they -cross the ABI by value as one serialized `(const uint8_t*, size_t)` -pair (see the [Value Buffer Protocol](reference/value-buffers.md)), -while `Enum` lowers by value as an integer. - -Every IR type derives `Debug`, `Clone`, `PartialEq`, `Serialize`, and -`Deserialize`. `Eq` is derived everywhere except `Api` itself, which -omits it because its `generators` overrides hold `toml::Value`, which -contains `f64`. +The document never learns what a `Named` reference *is*. That knowledge +lives one layer up, in `weaveffi_core`: + +- `weaveffi_core::resolved::ResolvedApi` wraps an `Api` that has passed + validation together with an index of every user-type declaration (bare + name to owning module path and `TypeKind`). It is the only way to reach + `BindingModel::build`, the `LanguageBackend` trait, and the + `Orchestrator`. `ResolvedApi::resolve(type_ref, module_path)` maps a + written reference to its resolved type; the document itself is never + mutated, so an IDL always round-trips through `extract` unchanged. +- `weaveffi_core::model::Ty` is the *resolved* type every generator + consumes: the same primitives and composites as `TypeRef`, but every + user type carries its kind and dot-qualified name (`Record`, `RichEnum`, + `Enum`, `Interface`, `TypedHandle`). There is no unresolved variant, so + a generator can't forget to handle one. +- Two total classifications hang off `Ty`. `Ty::family()` says how a + value crosses a *call boundary* (`Direct`, `String`, `Bytes`, `Buffer`, + `Object { nullable }`, `Iterator`); the ABI lowering, the marshalling + plan, and every backend's argument handling dispatch on it. + `Ty::wire()` says how a value is encoded *inside a value buffer* + (`WireType::Prim`, `Handle`, `Enum`, `User`, `Optional`, `List`, + `Map`); every backend's codec emitter dispatches on it. Records, rich + enums, optionals, lists, and maps are the `Buffer` family: they cross + the ABI by value as one serialized `(const uint8_t*, size_t)` pair (see + the [Value Buffer Protocol](reference/value-buffers.md)), while `Enum` + lowers by value as an integer. + +Every IR type derives `Debug`, `Clone`, `PartialEq`, `Eq`, `Serialize`, +and `Deserialize`. `TypeRef` (de)serializes as a string with custom syntax (`i32`, `handle`, `[T]`, `{K:V}`, `T?`, `&str`, `&[u8]`). The parser is @@ -201,15 +203,16 @@ JSON Schema export rely on it. ### Schema versioning -`CURRENT_SCHEMA_VERSION` (currently `"0.7.0"`) lives in +`CURRENT_SCHEMA_VERSION` (currently `"0.8.0"`) lives in [`crates/weaveffi-ir/src/ir.rs`][ir-source]. Pre-1.0, `SUPPORTED_VERSIONS` contains exactly the current version; older schema revisions are rejected by validation with an actionable error. When you change the schema: 1. Bump `CURRENT_SCHEMA_VERSION` (and the `weaveffi-ir` minor version). 2. Document the changes in `CHANGELOG.md` under a "Migration" section. -3. Update every sample IDL, the `weaveffi new` template, the README - quickstart, and the [Getting Started](getting-started.md) doc. +3. Update every fixture IDL under `crates/weaveffi-cli/tests/fixtures/`, + the README quickstart, and the [Getting Started](getting-started.md) + doc. The [stability page](stability.md#ir-schema-version-policy) is the external contract; this section is the implementation note. @@ -219,12 +222,12 @@ external contract; this section is the implementation note. `weaveffi_core::validate::validate_api` is the single entry point. It consumes the parsed `Api` and returns `Result`: on success the API has -passed every rule check and type-reference resolution, and the -`ResolvedApi` proof type is the only way into the generation stage; on -failure the diagnostics carry every error found, each renderable as an -actionable miette report. Advisory warnings are separate: -`validate::collect_warnings` returns `Vec`, which -the `lint` subcommand surfaces. +passed every rule check and every type reference has a declaration to +resolve to, and the `ResolvedApi` proof type is the only way into the +generation stage; on failure the diagnostics carry every error found, +each renderable as an actionable miette report. Advisory warnings are +separate: `validate::collect_warnings` returns `Vec`, +which `validate --warn` and `generate --warn` surface. Errors enforced today: @@ -239,10 +242,9 @@ Errors enforced today: - Structs must have at least one field; enums at least one variant; interfaces at least one member. - Enum discriminant uniqueness within an enum. -- Type references resolve by bare name across the whole API and are - qualified to their owning module during resolution, which also - rewrites each parsed `TypeRef::Named` reference into its resolved - kind (see [The IR](#the-ir) and +- Type references resolve by bare name across the whole API; the + `ResolvedApi` index qualifies each to its owning module and kind (see + [The IR](#the-ir) and [Cross-module references](reference/idl.md#cross-module-type-references)). - Interface members (constructors, methods, statics) share one namespace per interface; constructors declare no return and cannot be async; @@ -266,7 +268,6 @@ Warnings emitted today: - `DeepNesting` (composite types nested deeper than 3 levels). - `EmptyModuleDoc` (no `doc:` on any function in the module). - `AsyncVoidFunction` (async without a return type). -- `MutableOnValueType` (`mutable: true` on a non-pointer parameter). - `DeprecatedFunction` (informational). Interfaces, typed error domains, async functions, cancellable functions, @@ -291,24 +292,33 @@ from the TypeScript declarations) because Emscripten modules do not portably expose the trampoline machinery. Capability failures and mode gaps must stay loud: never skip a definition silently. -## Generator configuration resolution - -There is no single global config object. Each generator owns its own -typed `Generator::Config` (`CConfig`, `SwiftConfig`, `PythonConfig`, …), -so adding a knob to one target only touches that target's crate. The CLI -gathers all of them into one `CliConfig` struct (generated by the -`cli_targets!` macro, one field per target) and resolves it from three -sources (later wins): - -1. Defaults baked into each `Config::default()`. -2. The `--config ` external file passed to `generate`. -3. The inline `generators:` section of the IDL. - -The IDL section is the project-local source of truth and overrides any -machine-local TOML; see the -[Generator Configuration guide](guides/config.md#inline-generator-configuration). -Each resolved config is hashed (via `serde_json`) into the per-generator -cache key, so a config-only change re-runs just that target. +## Project configuration + +The API definition describes the API and nothing else. Package identity +and generator options live in `weaveffi.toml`, modelled by +`weaveffi_cli::config::ProjectConfig`: + +- `[package]` deserializes straight into `weaveffi_core::pkg::Package` + and is attached to the `ResolvedApi` (`ResolvedApi::with_package`), so + every generator reads the same identity through `api.package()` and + the shared `pkg::resolve` policy (explicit per-target key, then the + package name normalized per ecosystem, then the input file stem, then + the `weaveffi` default). For a `src/lib.rs` input the CLI substitutes + the crate directory name for the useless `lib` stem. +- `[global]` holds the cross-target knobs (`c_prefix`, + `strip_module_prefix`, and the `pre_generate`/`post_generate` hooks) + that `ProjectConfig::finalize` fans out to every per-target config. +- `[generators.]` deserializes into the `Targets` struct the + `cli_targets!` macro generates: one field per target, each the + generator crate's own `Config` type (`CConfig`, `SwiftConfig`, …), so + adding a knob to one target only touches that target's crate. + +Discovery mirrors Cargo: the CLI walks up from the input file's directory +to the first `weaveffi.toml`, or uses `--config `. The file uses +`deny_unknown_fields` throughout, so a misspelled table or key is an +error. Each resolved config is hashed (via `serde_json`) into the +per-target cache key alongside the `ResolvedApi` (package included), so a +config-only change re-runs just the targets it affects. ## Orchestrator @@ -316,59 +326,46 @@ cache key, so a config-only change re-runs just that target. 1. If `--force` is set, every cache entry under `{out_dir}/.weaveffi-cache/{target}.hash` is invalidated. -2. For each registered generator, the orchestrator hashes - `(api, generator.name(), config)` and compares against the persisted - hash, so an IR *or* config change re-runs just the affected target. +2. For each registered target, the orchestrator hashes + `(resolved api, target.name(), config)` and compares against the + persisted hash, so an IR, package, *or* config change re-runs just + the affected target. 3. If a `pre_generate` hook is configured (`OrchestratorHooks`), the orchestrator shells out to it (cmd on Windows, sh elsewhere) and aborts on non-zero exit. -4. The pending generators run **in parallel** via - `rayon::par_iter`. Generators must therefore be `Send + Sync`. -5. `post_generate` runs once after every generator has succeeded. -6. Each successful generator's hash is persisted. +4. The pending targets run **in parallel** via `rayon::par_iter`. + Backends must therefore be `Send + Sync`. +5. `post_generate` runs once after every target has succeeded. +6. Each successful target's hash is persisted. -This per-generator caching is what lets `weaveffi generate` skip every -target whose IR has not changed since the last run; see the -[Generator Configuration guide](guides/config.md#per-generator-incremental-cache). +This per-target caching is what lets `weaveffi generate` skip every +target whose inputs have not changed since the last run; see +[Project Configuration](guides/config.md#performance-and-caching). -## The `Generator` trait and the language-backend framework +## `LanguageBackend` and the `Target` view -The orchestrator consumes the object-safe `Generator` trait -(`weaveffi_core::codegen::Generator`). Each generator owns a typed, -serializable `Config`; the orchestrator stays config-agnostic by working -through the object-safe `DynGenerator` view: +There is one generator trait. Every target implements +`weaveffi_core::backend::LanguageBackend`, which owns a typed, +serializable `Config`; the orchestrator stays config-agnostic by storing +the object-safe `weaveffi_core::codegen::Target` view instead: ```rust,ignore -pub trait Generator: Send + Sync { - /// Per-target options. Must round-trip through `serde_json` so the - /// orchestrator can fold the config into the cache key. - type Config: Serialize + Default + Clone + Send + Sync; - - /// Stable short name (`"swift"`, `"c"`, …): the `--target` token and - /// the per-generator cache-file basename. - fn name(&self) -> &'static str; - - /// Render the bindings under `out_dir`. - fn generate(&self, api: &ResolvedApi, out_dir: &Utf8Path, config: &Self::Config) -> Result<()>; - - /// Files `generate` would write (used by `--dry-run` and `diff`). - fn output_files(&self, api: &ResolvedApi, out_dir: &Utf8Path, config: &Self::Config) -> Vec; +pub trait Target: Send + Sync { + fn name(&self) -> &'static str; // the `--target` token + fn capabilities(&self) -> TargetCapabilities; + fn generate(&self, api: &ResolvedApi, out_dir: &Utf8Path) -> Result<()>; + fn output_files(&self, api: &ResolvedApi, out_dir: &Utf8Path) -> Vec; + fn package(&self, api: &ResolvedApi, ctx: &PackageContext, out_dir: &Utf8Path) + -> Option>; + fn config_hash_input(&self) -> Vec; // folded into the cache key } ``` -To erase the associated `Config`, a typed generator is paired with a -concrete config value via `ConfiguredGenerator::new(gen, config)`, which -implements the object-safe `DynGenerator` trait the `Orchestrator` -stores. The CLI builds one `ConfiguredGenerator` per selected target -from the resolved `CliConfig`. - -### `LanguageBackend` and the shared driver - -Generators are **not** written against `Generator` directly. Each target -implements `weaveffi_core::backend::LanguageBackend` and is bridged to -`Generator` by the `impl_generator_via_backend!` macro, so the model -construction, the file I/O, and the `output_files` derivation live in one -place instead of being re-implemented eleven times: +`ConfiguredBackend::new(backend, config)` pairs a backend with a concrete +config value and implements `Target` by delegating to the shared driver. +The CLI builds one `ConfiguredBackend` per selected target from the +`Targets` struct; the driver does the model construction, the file I/O, +and the `output_files` derivation once instead of eleven times: ```rust,ignore pub trait LanguageBackend: Send + Sync { @@ -398,8 +395,9 @@ pub trait LanguageBackend: Send + Sync { The free `backend::run` builds the `BindingModel` once (with the backend's `prefix`), calls `files`, and writes each `OutputFile` (creating parent directories). `backend::output_files` calls the same -`files` and returns the sorted path list, so `generate` and -`output_files` are derived from a single source and **cannot drift**. +`files` and returns the sorted path list, and `backend::package_files` +drives the optional `package` hook, so `generate`, `output_files`, and +`package` are derived from a single source and **cannot drift**. Python, Ruby, Go, Dart, and .NET are single-pass backends: they override the per-entity hooks and compose `emit_members` inside their module scoping. The rest build their layout directly in `files`: C and @@ -415,24 +413,28 @@ managed by the `CodeWriter` toolkit (see below) rather than by hand-rolled `\n`/space bookkeeping. Shared rendering infrastructure lives in `weaveffi_core`: -- `backend`: the `LanguageBackend` trait, the `run`/`output_files` - driver, the `OutputFile` type, and the `impl_generator_via_backend!` - bridge macro. +- `backend`: the `LanguageBackend` trait, the `run`/`output_files`/ + `package_files` driver, and the `OutputFile` type. - `model::BindingModel`: the normalized, fully-lowered view every - backend renders from (precomputed C symbol names and ABI signatures). + backend renders from (precomputed C symbol names and ABI signatures, + every type a resolved `model::Ty`), plus the `roots`/`children` + module-tree walkers and the `any_type` predicate scan. - `codegen::writer::CodeWriter`: the structured code-emission toolkit (see [The `CodeWriter` emission toolkit](#the-codewriter-emission-toolkit)). -- `codegen::common`: module-tree traversal (`walk_modules`, +- `codegen::common`: IR module-tree traversal (`walk_modules`, `walk_modules_with_path`), doc-comment emission (`emit_doc`), and `pascal_case` naming. - `plan`: the marshalling plan, the language-neutral calling contracts every backend renders (see [The marshalling plan](#the-marshalling-plan)). -- `wire`: the canonical classification of every IR type into its wire - shape inside a value buffer (`wire::classify` folds a `TypeRef` into a - `WireType`), so backend codec emitters dispatch on one closed alphabet - instead of each re-deriving the folds (handles encode as `u64` tokens, - borrowed views encode like their owned forms, records and rich enums - share one user-codec shape). +- `model::ty`: `Ty::wire()` folds every type into its wire shape inside + a value buffer (`WireType`), so backend codec emitters dispatch on one + closed alphabet instead of each re-deriving the folds (handles encode + as `u64` tokens, borrowed views encode like their owned forms, records + and rich enums share one user-codec shape). +- `cabi`: the shared renderer for C declarations (runtime prototypes, + including `{prefix}_abi_version` and the `{PREFIX}_ABI_VERSION` macro, + enums, interface typedefs, function prototypes) that both the C and C++ + headers are produced from, so the two cannot drift. - `lang`: per-language reserved-word tables (`PYTHON_KEYWORDS`, `GO_KEYWORDS`, …) and the single `escape_ident` rule (a reserved name gains a trailing underscore), so a field named `type` or a parameter @@ -505,7 +507,27 @@ The `c_prefix` configuration is honored end-to-end: when set, the generated C output uses it consistently, including references to `weaveffi-abi` runtime symbols (`{c_prefix}_error`, `{c_prefix}_error_clear`, `{c_prefix}_free_string`, -`{c_prefix}_free_bytes`). +`{c_prefix}_free_bytes`, `{c_prefix}_abi_version`). Because the Rust +runtime always exports the canonical `weaveffi_*` names, the header +`#define`s each prefixed runtime name to its canonical symbol (the +`utils::ABI_RUNTIME_SYMBOLS` table, kept in lockstep with +`export_runtime!`). + +### ABI revision + +`weaveffi_abi::ABI_VERSION` (mirrored as `weaveffi_core::cabi::ABI_VERSION` +and kept equal by a test in the `weaveffi` facade crate) numbers the +runtime surface: the `weaveffi_error` layout, the value-buffer encoding, +and the set and signatures of the `weaveffi_*` runtime symbols. +`export_runtime!` exports it as `weaveffi_abi_version()`; the C header +declares it and defines `{PREFIX}_ABI_VERSION`. Generated consumers embed +the revision they were built for and, where a check costs one call at +load time, compare it before touching anything else: Python raises +`ImportError`, Ruby `LoadError`, Dart `StateError`, Go panics in `init`, +.NET throws from the `NativeMethods` static constructor, the Node addon +throws from `Init`, and the Wasm loader throws before returning the API +object. A producer that predates versioning fails the same way (missing +symbol), so the mismatch is never a garbled value buffer later. Interface lifecycle symbols and enum constants follow the patterns in the [C generator reference](generators/c.md). @@ -516,8 +538,8 @@ The C ABI is the foundation every binding sits on: a flat, C-callable surface where each IDL type lowers to a fixed sequence of C parameters. A `string` becomes one `const char*`; `bytes` becomes `const uint8_t* {name}_ptr, size_t {name}_len`; every buffered type -(records, rich enums, optionals, lists, maps; see -[`is_buffered`][abi-source]) becomes the same borrowed +(records, rich enums, optionals, lists, maps; the `Buffer` +[`Family`](#the-ir)) becomes the same borrowed `{name}_ptr` / `{name}_len` pair holding its serialized [value buffer](reference/value-buffers.md); bytes and buffered returns append a `size_t* out_len` pointer; and every fallible call ends with a @@ -560,10 +582,9 @@ their generator rather than leaking into the shared model: generator. Imperative generators (Go cgo, Node, Dart, Swift) build their FFI -signatures inline with marshalling code and share the single -`is_c_pointer_type` classifier in `weaveffi_core::codegen::common`. The -Android (JNI) and Wasm backends target different ABIs entirely and do -not consume the C lowering. +signatures inline with marshalling code and dispatch on `Ty::family()` +for the shape of each slot. The Android (JNI) and Wasm backends target +different ABIs entirely and do not consume the C lowering. When you add a parameter shape or change how a type crosses the boundary, change `weaveffi_core::abi` and let the consumers inherit it; @@ -582,8 +603,8 @@ independently (and inconsistently): (`plan::ret_pass`): how one parameter or return crosses the boundary (a direct scalar slot, a string pointer, a bytes or buffer `ptr`/`len` pair, or an object handle), so marshalling code dispatches - on the plan instead of re-deriving the shape from `TypeRef` with local - `is_buffered` checks. + on the plan instead of re-deriving the shape with local matches on + `Ty`. - **Errors.** `ErrorStrategy` (`Throws` | `Trap`): when a call reports through `out_err`, is the non-zero code a typed domain error the caller can catch (`throws: true`), or a producer bug the wrapper must @@ -631,7 +652,7 @@ doesn't need): | ------------- | ----------------------------------------------------------------- | | `types.rs` | Target type mapping, identifier naming and keyword escaping | | `docs.rs` | Doc-comment emission in the target's comment syntax | -| `codec.rs` | Value-buffer encode/decode emitters, dispatched on `wire::classify` | +| `codec.rs` | Value-buffer encode/decode emitters, dispatched on `Ty::wire()` | | `runtime.rs` | The fixed runtime prelude the generated code relies on | | `calls.rs` | Sync/async/iterator wrappers, callbacks, listeners, marshalling | | `entities.rs` | Enums, records, interfaces, typed error domains | @@ -658,10 +679,15 @@ non-deterministically on different platforms or insta orderings. ## Snapshot tests `crates/weaveffi-cli/tests/snapshots.rs` runs every generator across a -ten-fixture corpus (`tests/fixtures/01_calculator` … `10_shapes`: -calculator, contacts, inventory, async-demo, events, kitchen-sink, -docs-everywhere, kvstore, nested-modules, and shapes). Output is diffed via -[`cargo-insta`][insta]. When a snapshot diff is intentional: +five-fixture corpus under `tests/fixtures/` (`kitchen_sink`, which +exercises every IDL feature; `shapes`, rich enums and records; +`nested_modules`, cross-module references; `docs_everywhere`, doc-comment +emission; and `edge_cases`, reserved-word identifiers, deeply nested +composites, mutable and optional parameters, and the other shapes a +mainstream API never reaches). The eleven targets are driven by one data-driven +`snapshot_tests!` macro, so adding a fixture or a target is a one-line +change. Output is diffed via [`cargo-insta`][insta]. When a snapshot diff +is intentional: ```bash cargo install cargo-insta --locked @@ -691,21 +717,18 @@ A condensed checklist (the long version lives in a `c_prefix`), and `files` (returning every `OutputFile`). For a single-pass layout, override the `render_enum`/`render_struct`/ `render_function` hooks and compose `emit_members`; otherwise build the - layout directly in `files`. Then add - `weaveffi_core::impl_generator_via_backend!();` to bridge it - to `Generator` (this derives `generate` and `output_files`). Reuse - `BindingModel` and `weaveffi_core::codegen::common` instead of - re-deriving traversal or ABI classification. + layout directly in `files`. Reuse `BindingModel`, `Ty::family()`, and + `Ty::wire()` instead of re-deriving traversal or ABI classification. 3. Wire the generator into the `cli_targets!` registry macro in `crates/weaveffi-cli/src/config.rs`: add one line (`"" => : via `, plus `strip` if the generator honors `strip_module_prefix`). That single entry is the - source of truth: it expands to the `CliConfig` field, the - `--target ` parser entry, inline-config merging, and the - `Orchestrator` registration. No other CLI edits are required. -4. Add snapshot fixtures in `crates/weaveffi-cli/tests/snapshots.rs` - covering at minimum the calculator, contacts, inventory, - async-demo, and events sample IDLs. + source of truth: it expands to the `Targets` field (the + `[generators.]` table), the `--target ` parser entry, and + the `Orchestrator` registration. No other CLI edits are required. +4. Add the target to the `snapshot_tests!` invocation in + `crates/weaveffi-cli/tests/snapshots.rs` and to the determinism test; + every fixture in the corpus then runs against it. 5. Document the generator under `docs/src/generators/.md` and link it from `docs/src/SUMMARY.md`. 6. Add conformance consumers under `conformance//` and wire them @@ -717,8 +740,8 @@ A condensed checklist (the long version lives in - [IDL Schema](reference/idl.md): the type system and validation rules from a user's perspective. -- [Generator Configuration](guides/config.md): every option a - consumer can set. +- [Project Configuration](guides/config.md): every option a + consumer can set in `weaveffi.toml`. - [Stability and Versioning](stability.md): what counts as a breaking change once we hit 1.0. - [Memory Ownership](guides/memory.md): the per-target memory rules diff --git a/docs/src/comparison.md b/docs/src/comparison.md index de15f0e3..9d2a4956 100644 --- a/docs/src/comparison.md +++ b/docs/src/comparison.md @@ -105,9 +105,9 @@ WeaveFFI is the right pick when you want: self-contained: a Swift consumer adds your `.xcframework` + a SwiftPM manifest and is done. No "install WeaveFFI" step on the consumer side. 3. **A native library that isn't (only) Rust.** WeaveFFI works against - anything that exposes a stable C ABI: Rust (with `--scaffold` - convenience), C, C++, Zig, etc. UniFFI and diplomat assume Rust; - autocxx assumes C++. + anything that exposes a stable C ABI: Rust (with the + `#[weaveffi::module]` macro generating the ABI for you), C, C++, Zig, + etc. UniFFI and diplomat assume Rust; autocxx assumes C++. 4. **Idiomatic per-target output, not a lowest-common-denominator API.** Async functions become `async/await` in Swift, `Promise`s in Node, `suspend fun` in Kotlin, `async def` in Python, and `Task` in C#, diff --git a/docs/src/faq.md b/docs/src/faq.md index 50db7fdc..0b16c12a 100644 --- a/docs/src/faq.md +++ b/docs/src/faq.md @@ -130,11 +130,11 @@ panics or traps instead of surfacing an error type. See the Yes, via two escape hatches in increasing order of power: -1. **Generator config** (`--config cfg.toml` or inline `generators:` - table in the IDL). Controls Swift module names, Android package, - C prefix, C++ namespace, Dart/Go/Ruby package names, module-prefix - stripping (`strip_module_prefix`), and other per-target knobs. See - the [Generator Configuration guide](guides/config.md). +1. **Project config** (`[generators.]` tables in the + `weaveffi.toml` next to your definition). Controls Swift module + names, Android package, C prefix, C++ namespace, Dart/Go/Ruby package + names, module-prefix stripping (`strip_module_prefix`), and other + per-target knobs. See the [Project Configuration guide](guides/config.md). 2. **Hook commands** (`pre_generate` / `post_generate` in the config). Run arbitrary shell commands before and after generation, useful for `prettier`, `swiftformat`, `gofmt`, etc. @@ -202,9 +202,9 @@ ship it alongside the generated package. Three common patterns: The name, version, and metadata stamped into every generated manifest (`package.json`, `pyproject.toml`, `*.gemspec`, `*.csproj`, `pubspec.yaml`, -`Package.swift`, `go.mod`, ...) come from a single -[`package:` block](reference/idl.md#package-metadata) in your IDL, so you set -your identity once and every ecosystem stays in sync. +`Package.swift`, `go.mod`, ...) come from the single +[`[package]` table](guides/config.md#package) of your `weaveffi.toml`, so you +set your identity once and every ecosystem stays in sync. There is no opinionated "weaveffi publish" command today; you use each ecosystem's normal publish flow. The diff --git a/docs/src/generators/android.md b/docs/src/generators/android.md index ebf71a43..970ad6ed 100644 --- a/docs/src/generators/android.md +++ b/docs/src/generators/android.md @@ -47,7 +47,7 @@ layer bridges them to the C ABI. ## Example IDL → generated code ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: contacts enums: @@ -78,7 +78,7 @@ modules: The Kotlin wrapper declares `external fun` entries inside a companion object and loads the JNI library on first use. Function names are lowerCamelCase with the module prefix stripped by default -(`strip_module_prefix = false` in `[android]` restores prefixed names). +(`strip_module_prefix = false` in `[generators.android]` restores prefixed names). Where a parameter or return value needs wrapping (enums, buffered values), the external entry is a private `...Jni` function with lowered types and a public wrapper converts at the boundary. A buffered value diff --git a/docs/src/generators/c.md b/docs/src/generators/c.md index 07b5210f..6774fac2 100644 --- a/docs/src/generators/c.md +++ b/docs/src/generators/c.md @@ -79,7 +79,7 @@ helpers, is rewritten with the new prefix. ## Example IDL → generated code ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: contacts enums: @@ -374,7 +374,7 @@ macOS: ```bash cargo build -p contacts -weaveffi generate samples/contacts/contacts.yml -o generated +weaveffi generate samples/contacts/src/lib.rs -o generated cc -I generated/c conformance/c/contacts.c -L target/debug -lcontacts -o c_contacts DYLD_LIBRARY_PATH=target/debug ./c_contacts @@ -384,7 +384,7 @@ Linux: ```bash cargo build -p contacts -weaveffi generate samples/contacts/contacts.yml -o generated +weaveffi generate samples/contacts/src/lib.rs -o generated cc -I generated/c conformance/c/contacts.c -L target/debug -lcontacts -o c_contacts LD_LIBRARY_PATH=target/debug ./c_contacts @@ -394,7 +394,7 @@ Windows: ```powershell cargo build -p contacts -weaveffi generate samples\contacts\contacts.yml -o generated +weaveffi generate samples\contacts\src\lib.rs -o generated cl /I generated\c conformance\c\contacts.c /link contacts.lib .\contacts.exe ``` diff --git a/docs/src/generators/cpp.md b/docs/src/generators/cpp.md index b21be933..293bcd39 100644 --- a/docs/src/generators/cpp.md +++ b/docs/src/generators/cpp.md @@ -48,7 +48,7 @@ directory can be dropped into any CMake build. ## Example IDL → generated code ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: contacts enums: diff --git a/docs/src/generators/dart.md b/docs/src/generators/dart.md index cfdf1a42..60f1dad2 100644 --- a/docs/src/generators/dart.md +++ b/docs/src/generators/dart.md @@ -48,7 +48,7 @@ Booleans cross as `Int32` (`0`/`1`) and the wrapper converts both ways. ## Example IDL → generated code ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: contacts enums: diff --git a/docs/src/generators/dotnet.md b/docs/src/generators/dotnet.md index 92d93e26..af1d771e 100644 --- a/docs/src/generators/dotnet.md +++ b/docs/src/generators/dotnet.md @@ -52,7 +52,7 @@ package named `kvstore` produces `Kvstore.cs` inside ## Example IDL → generated code ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: contacts enums: diff --git a/docs/src/generators/go.md b/docs/src/generators/go.md index c67c26cf..256982e2 100644 --- a/docs/src/generators/go.md +++ b/docs/src/generators/go.md @@ -53,7 +53,7 @@ Booleans map to `C._Bool`, matching CGo's representation of `_Bool`. ## Example IDL → generated code ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: contacts enums: @@ -181,22 +181,12 @@ C.weaveffi_free_bytes(raw, cOutLen) ids := /* generated unpack routine over buf */ ``` -The Go module path defaults to `weaveffi`; override it via the -generator config: +The Go module path follows the `[package]` name in `weaveffi.toml` +(falling back to `weaveffi`); override it directly with: -```yaml -version: "0.7.0" -modules: - - name: math - functions: - - name: add - params: - - { name: a, type: i32 } - - { name: b, type: i32 } - return: i32 -generators: - go: - module_path: "github.com/myorg/mylib" +```toml +[generators.go] +module_path = "github.com/myorg/mylib" ``` ## Typed errors diff --git a/docs/src/generators/node.md b/docs/src/generators/node.md index 19b70428..e5bad8b6 100644 --- a/docs/src/generators/node.md +++ b/docs/src/generators/node.md @@ -51,7 +51,7 @@ camelCased function wrappers. ## Example IDL → generated code ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: contacts enums: @@ -109,7 +109,7 @@ export enum Color { ``` Functions are exported flat in lowerCamelCase with the module prefix -stripped by default (`strip_module_prefix = false` in `[node]` restores +stripped by default (`strip_module_prefix = false` in `[generators.node]` restores `_`-prefixed names); parameters are camelCased too. Optional return and parameter types use `| null`, arrays use `T[]`: @@ -293,7 +293,7 @@ producer cdylib on the linker path: ```bash cargo build -p kvstore -weaveffi generate samples/kvstore/kvstore.yml -o generated +weaveffi generate samples/kvstore/src/lib.rs -o generated cd generated/node npm install # builds build/Release/weaveffi.node diff --git a/docs/src/generators/python.md b/docs/src/generators/python.md index 1dc876d7..99e3cc14 100644 --- a/docs/src/generators/python.md +++ b/docs/src/generators/python.md @@ -65,7 +65,7 @@ standard fixed-width boolean type across ABIs. ## Example IDL → generated code ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: contacts enums: diff --git a/docs/src/generators/ruby.md b/docs/src/generators/ruby.md index e0a00b37..6ee4c0f7 100644 --- a/docs/src/generators/ruby.md +++ b/docs/src/generators/ruby.md @@ -57,7 +57,7 @@ directions. ## Example IDL → generated code ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: contacts enums: @@ -464,11 +464,10 @@ write and read the tagged wire format. - Windows: place `weaveffi.dll` next to the script or add its directory to `PATH`. -The Ruby module name and gem name can be customised via generator -configuration: +The Ruby module name and gem name can be customised in `weaveffi.toml`: ```toml -[ruby] +[generators.ruby] module_name = "MyBindings" gem_name = "my_bindings" ``` diff --git a/docs/src/generators/swift.md b/docs/src/generators/swift.md index 04cb1e15..d6b11881 100644 --- a/docs/src/generators/swift.md +++ b/docs/src/generators/swift.md @@ -16,8 +16,8 @@ Swift module (`WeaveFFI`) that wraps the C ABI in idiomatic Swift with | `generated/swift/Sources/WeaveFFI/WeaveFFI.swift` | Swift wrapper: enums, struct classes, namespaced module functions | The module name shown above (`WeaveFFI`) is the default. It is overridden by -`[swift] module_name` or, failing that, by the IDL -[`package:` name](../reference/idl.md#package-metadata) PascalCased +`[generators.swift] module_name` in `weaveffi.toml` or, failing that, by the +[`[package]` name](../guides/config.md#package) PascalCased (`async-demo` → `AsyncDemo`). The Swift wrapper, its `Sources//` directory, the system-library target, and its `Sources/C/` module map all move together (e.g. `AsyncDemo` + `CAsyncDemo`), so the generated package @@ -53,7 +53,7 @@ stays buildable under any name. ## Example IDL → generated code ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: contacts enums: @@ -133,7 +133,7 @@ public struct Contact { Module functions live as static methods on a namespace enum in lowerCamelCase with real argument labels; the module prefix is stripped by -default (`strip_module_prefix = false` in `[swift]` restores it), since the +default (`strip_module_prefix = false` in `[generators.swift]` restores it), since the namespace enum already scopes the name. A function with `throws: true` becomes a Swift `throws` function delivering the typed domain error. String parameters are passed as NUL-terminated C strings via `withCString`: @@ -336,7 +336,7 @@ its module resolves to `Contacts` + `CContacts` from the package name): ```bash cargo build -p contacts -weaveffi generate samples/contacts/contacts.yml -o generated +weaveffi generate samples/contacts/src/lib.rs -o generated swiftc \ -I generated/swift/Sources/CContacts \ diff --git a/docs/src/generators/wasm.md b/docs/src/generators/wasm.md index db4e571d..7263c1a5 100644 --- a/docs/src/generators/wasm.md +++ b/docs/src/generators/wasm.md @@ -416,18 +416,10 @@ exports as `Module['_name']` rather than `instance.exports.name`. Set ```toml # weaveffi.toml -[wasm] +[generators.wasm] emscripten = true ``` -or inline in the IDL: - -```yaml -generators: - wasm: - emscripten: true -``` - Instead of a URL, the loader accepts the initialized Emscripten module, or the promise returned by its `MODULARIZE` factory. You construct the module yourself, so options like `locateFile` stay under your control: diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md index 50695db5..5c997486 100644 --- a/docs/src/getting-started.md +++ b/docs/src/getting-started.md @@ -40,10 +40,7 @@ Describe the API once in a language-neutral IDL. Create `math.yml` with a record and a function: ```yaml -version: "0.7.0" -package: - name: my-math - version: "0.1.0" +version: "0.8.0" modules: - name: math structs: @@ -59,18 +56,27 @@ modules: return: i32 ``` -The optional `package:` block sets the name and version stamped into every -generated package manifest (`package.json`, `pyproject.toml`, `Package.swift`, -and so on). The IDL also supports primitives (`i32`, `f64`, `bool`, `string`, -`bytes`, `handle`), optionals (`string?`), lists (`[i32]`), interfaces -(objects with constructors, methods, and statics), and typed error domains -(opt in per function with `throws: true`). See the -[IDL Schema](reference/idl.md#package-metadata) reference for the full -specification. +The IDL describes only the API. It supports primitives (`i32`, `f64`, `bool`, +`string`, `bytes`, `handle`), optionals (`string?`), lists (`[i32]`), +interfaces (objects with constructors, methods, and statics), and typed error +domains (opt in per function with `throws: true`). See the +[IDL Schema](reference/idl.md) reference for the full specification. -> **Prefer not to hand-write YAML?** Run `weaveffi new my-project` to scaffold a -> starter project (an example IDL plus a `Cargo.toml` and `src/lib.rs` stub) you -> can edit instead. +Everything about how the API is *published* lives in an optional +`weaveffi.toml` next to it. Create one so the generated package manifests +(`package.json`, `pyproject.toml`, `Package.swift`, and so on) carry your name +and version: + +```toml +[package] +name = "my-math" +version = "0.1.0" +``` + +The CLI picks up the nearest `weaveffi.toml` at or above the input file +automatically; pass `--config` to point at a different one. The same file +holds per-target options under `[generators.]` tables; see +[Configuration](guides/config.md). > **Writing a Rust producer?** You can make annotated Rust the single source of > truth instead of a separate IDL: annotate a module with `#[weaveffi::module]` @@ -84,20 +90,25 @@ specification. Run the generator to produce bindings for all targets: ```bash -weaveffi generate math.yml -o generated --scaffold +weaveffi generate math.yml -o generated ``` -The `--scaffold` flag also emits a `scaffold.rs` with Rust FFI stubs you can -use as a starting point. The output tree looks like: +Pass `--target c,swift,node` to generate a subset. The output tree has one +directory per target: ```text generated/ ├── c/ # C header + convenience stubs +├── cpp/ # RAII C++ header + CMakeLists.txt ├── swift/ # SwiftPM package + Swift wrapper ├── android/ # Kotlin JNI wrapper + Gradle skeleton -├── node/ # N-API loader + TypeScript types -├── wasm/ # Wasm loader stub -└── scaffold.rs # Rust FFI function stubs +├── node/ # N-API addon + TypeScript types +├── wasm/ # JavaScript loader + TypeScript types +├── python/ # ctypes bindings + .pyi stubs +├── dotnet/ # C# P/Invoke bindings +├── dart/ # dart:ffi bindings +├── go/ # cgo bindings +└── ruby/ # FFI gem bindings ``` ## 4) Examine the generated output @@ -161,9 +172,8 @@ export function add(a: number, b: number): number The generated C header (`generated/c/weaveffi.h`) is the contract your native library must satisfy, and it's the same contract every language binding calls into. You can implement it in any language that can expose a C ABI; here we use -Rust, starting from the generated `scaffold.rs`, which already contains a -`#[no_mangle] extern "C"` stub (with a `todo!()` body) for every symbol in the -header. +Rust and write the one `#[no_mangle] extern "C"` function the header declares +by hand. Create a library crate, add the WeaveFFI ABI helpers, and build a `cdylib`: @@ -180,8 +190,7 @@ In `Cargo.toml`: crate-type = ["cdylib"] ``` -Copy `scaffold.rs` into `src/lib.rs` and fill in the bodies. Implementing `add` -looks like this: +Implementing `add` in `src/lib.rs` looks like this: ```rust #![allow(unsafe_code)] @@ -199,9 +208,9 @@ pub extern "C" fn weaveffi_math_add( a + b } -// Emit the fixed WeaveFFI C ABI runtime surface (free_string, free_bytes, -// error_clear, cancel_token_*) in one line. Call this exactly once per -// cdylib. +// Emit the fixed WeaveFFI C ABI runtime surface (abi_version, free_string, +// free_bytes, error_clear, cancel_token_*) in one line. Call this exactly +// once per cdylib. abi::export_runtime!(); ``` @@ -214,6 +223,9 @@ Key points: - The library must export the WeaveFFI runtime symbols: invoke [`weaveffi_abi::export_runtime!()`][export-runtime-doc] to emit all of them in one line instead of writing each `#[no_mangle]` thunk by hand. + Among them is `weaveffi_abi_version()`, which the generated Python, Ruby, + Dart, Go, .NET, Node.js, and Wasm bindings call at load time to refuse a + library built against a different ABI revision. [export-runtime-doc]: https://docs.rs/weaveffi-abi/latest/weaveffi_abi/macro.export_runtime.html @@ -276,64 +288,14 @@ add(3, 4) = 7 ## Next steps -- Run `weaveffi doctor` to check which platform toolchains are available. - Read the [IDL Schema](reference/idl.md) reference for all supported types and features. - Writing a Rust producer? See - [The Rust Producer Macro](guides/producer-macro.md) to skip the scaffold and - generate the C ABI directly from annotated Rust. + [The Rust Producer Macro](guides/producer-macro.md) to generate the C ABI + directly from annotated Rust instead of implementing the header by hand. - See the [Calculator tutorial](tutorials/calculator.md) for a full end-to-end walkthrough including Swift and Node.js. - Explore the [Generators](generators/README.md) section for target-specific - details. - -## Checking a single target - -`weaveffi doctor` runs every toolchain check it knows about. To narrow it -down to a single target, pass `--target {name}`: - -```bash -weaveffi doctor --target dart -weaveffi doctor --target cpp -weaveffi doctor --target go -weaveffi doctor --target ruby -weaveffi doctor --target dotnet -weaveffi doctor --target python -weaveffi doctor --target swift -weaveffi doctor --target android -weaveffi doctor --target node -weaveffi doctor --target wasm -``` - -Only checks whose `applies_to` set contains the chosen target (plus the -required Rust toolchain, which always runs) are executed. When `--target` -is set the command exits with a non-zero status if any of those checks -failed, making it scriptable in CI: - -```bash -if ! weaveffi doctor --target dart; then - echo "Dart toolchain not ready" >&2 - exit 1 -fi -``` - -For machine-readable output (handy for piping into `jq` or aggregating -results across CI matrices), use `--format json`: - -```bash -weaveffi doctor --target ruby --format json | jq '.[] | select(.ok == false)' -``` - -Each entry has `id`, `name`, `ok`, `version`, `hint`, and `applies_to` fields. - -## Generating Man Pages - -The CLI can generate its own man pages. Pass an output directory to `--out`: - -```bash -mkdir -p ./man -weaveffi man --out ./man -man ./man/weaveffi.1 -``` - -This writes `weaveffi.1` as well as one man page per subcommand (e.g., `weaveffi-generate.1`) into the specified directory. + details, and [Configuration](guides/config.md) for `weaveffi.toml`. +- Add `weaveffi diff --check` to CI so regenerated bindings can't drift from + the committed ones; see [Stability and Versioning](stability.md). diff --git a/docs/src/guides/README.md b/docs/src/guides/README.md index 4efc4fc6..46ac3c9e 100644 --- a/docs/src/guides/README.md +++ b/docs/src/guides/README.md @@ -7,5 +7,5 @@ Practical guides for working with WeaveFFI bindings across targets. - [Error Handling](errors.md): the uniform error model and how each target surfaces failures. - [Async Functions](async.md): IDL declaration, the C ABI callback contract, and per-target async surfaces. - [Annotated Rust Extraction](extract.md): extract an IDL from annotated Rust source instead of writing YAML by hand. -- [Generator Configuration](config.md): customize per-target names, module-prefix stripping, and the C ABI prefix via `weaveffi.toml` or inline `generators:` blocks. +- [Project Configuration](config.md): package identity, per-target names, module-prefix stripping, and the C ABI prefix via `weaveffi.toml`. - [Packaging and Distribution](packaging.md): assemble ready-to-publish packages that bundle prebuilt native libraries per platform. diff --git a/docs/src/guides/async.md b/docs/src/guides/async.md index e230f63d..2a698cb2 100644 --- a/docs/src/guides/async.md +++ b/docs/src/guides/async.md @@ -43,7 +43,7 @@ Avoid async for: ### 1. Declare the function in the IDL ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: net errors: diff --git a/docs/src/guides/config.md b/docs/src/guides/config.md index 9aae1892..e7e9daee 100644 --- a/docs/src/guides/config.md +++ b/docs/src/guides/config.md @@ -1,300 +1,247 @@ -# Generator Configuration +# Project Configuration ## Overview -WeaveFFI ships with sensible defaults so `weaveffi generate api.yml` -just works. When you need to override package names, namespaces, or -the C ABI prefix, you have two options that compose with each other: - -- A TOML file (`weaveffi.toml`) passed via `--config`. Per-environment - values that vary by machine or CI runner. -- An inline `generators:` block inside the IDL. Project-wide values - every contributor inherits without remembering a flag. - -When the same option appears in both, the inline IDL value wins. - -## When to use - -- Use the **TOML config** when one developer or one pipeline needs to - swap a value without changing the IDL. -- Use the **inline `generators:` block** when the value is part of the - project contract (Swift module name, Go module path, custom - C ABI prefix). Checking it into the IDL guarantees consistency. -- Use **both** when there is a project-wide default that an environment - occasionally needs to override. - -## Step-by-step - -### 1. Pass a TOML config file - -```bash -weaveffi generate api.yml -o generated --config weaveffi.toml -``` +WeaveFFI ships with sensible defaults so `weaveffi generate src/lib.rs` (or +`weaveffi generate api.yml`) just works. The API definition, whether it's an +annotated Rust module or an IDL document, describes only the API. Everything +about how that API is published lives in one optional file next to it: +`weaveffi.toml`. ```toml -[swift] -module_name = "MyApp" +[package] +name = "kvstore" +version = "1.2.0" +description = "An embedded key-value store" +license = "MIT" +authors = ["Example "] +repository = "https://github.com/example/kvstore" -[android] -package = "com.example.myapp" - -[node] -package_name = "@myorg/myapp" - -[wasm] -module_name = "myapp_wasm" +[global] +c_prefix = "kv" -[c] -prefix = "myapp" +[generators.swift] +module_name = "KVStore" -[global] -strip_module_prefix = false +[generators.android] +package = "com.example.kvstore" ``` -Every section and key is optional; omit anything you want defaulted. -The `[global]` table accepts the alias `[weaveffi]`. Module-prefix -stripping is on by default, so the useful direction for -`strip_module_prefix` is `false`: one `[global]` line restores -module-prefixed wrapper names across every supporting target. +The file has three tables, all optional: -### 2. Embed `generators:` in the IDL +- `[package]`: the distribution identity stamped into every generated + manifest (`package.json`, `pyproject.toml`, `Package.swift`, `.csproj`, + `pubspec.yaml`, `go.mod`, `.gemspec`, and so on). +- `[global]`: knobs that affect several generators or the orchestrator + itself. +- `[generators.]`: one table per language with that generator's own + options. -```yaml -version: "0.7.0" -modules: - - name: math - functions: - - name: add - params: - - { name: a, type: i32 } - - { name: b, type: i32 } - return: i32 -generators: - swift: - module_name: MyAppFFI - android: - package: com.example.myapp - c: - prefix: myapp - cpp: - namespace: myapp - header_name: myapp.hpp - standard: "20" - dart: - package_name: my_dart_pkg - go: - module_path: github.com/example/myapp - ruby: - module_name: MyApp - gem_name: myapp - weaveffi: - strip_module_prefix: false - pre_generate: "cargo build --release" -``` +## Discovery -Unknown target keys are silently ignored, so an older `weaveffi` CLI -can still read an IDL written for a newer one. - -### 3. Verify the result +`weaveffi generate`, `weaveffi package`, and `weaveffi diff` find the config +the same way Cargo finds `Cargo.toml`: they walk up from the input file's +directory and use the first `weaveffi.toml` they meet. Keeping the file beside +`Cargo.toml` (for a Rust producer) or beside the IDL therefore needs no flag. ```bash -weaveffi generate api.yml -o generated --config weaveffi.toml -ls generated/ +weaveffi generate src/lib.rs -o generated # nearest weaveffi.toml +weaveffi generate src/lib.rs -o generated --config ci.toml # a specific file ``` -For day-to-day project recipes: +Without any config file, every generator runs with its defaults and the +package name falls back as described under [Package identity](#package-identity). + +Unknown tables and keys are rejected, so a typo like `[generator.swift]` or +`modulename` fails the run instead of silently doing nothing. + +## `[package]` + +| Key | Type | Description | +|---------------|------------|------------------------------------------------------------------| +| `name` | string | Distribution name; normalized per ecosystem (see below) | +| `version` | string | Published version (default `0.1.0`) | +| `description` | string | One-line description for manifests that carry one | +| `license` | string | SPDX license expression | +| `authors` | `[string]` | Author strings, `Name ` | +| `homepage` | string | Project home page URL | +| `repository` | string | Source repository URL | + +### Package identity + +Every generated manifest resolves its name through one shared policy. For +an identity value, an explicit per-target key wins; otherwise the generator +uses `package.name` normalized for its ecosystem (`kvstore` becomes +`Kvstore` for Swift and .NET, `kvstore` for Python and Ruby, and so on); +otherwise it falls back to the input file stem, and finally to the +`weaveffi`/`WeaveFFI` default. The per-target keys that participate are +`[generators.swift] module_name`, `[generators.node] package_name`, +`[generators.python] package_name`, `[generators.dart] package_name`, +`[generators.go] module_path`, `[generators.ruby] gem_name`, and +`[generators.dotnet] namespace` (which also sets the NuGet package id). + +For a Rust producer whose input is `src/lib.rs`, the input file stem (`lib`) +is useless as a name, so the CLI uses the crate directory name instead +(`my-crate/src/lib.rs` yields `my-crate`) when `[package] name` is unset. + +## `[global]` + +| Key | Type | Default | Description | +|-----------------------|--------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `c_prefix` | string | unset | The C ABI symbol prefix (`{prefix}_{module}_{function}`), fanned out to every target that hasn't set its own `prefix`; wins over `[generators.c] prefix` | +| `strip_module_prefix` | bool | unset | Sets `strip_module_prefix` on every target that supports it, overriding their tables. Stripping is on by default, so `false` restores module-prefixed names everywhere | +| `pre_generate` | string | none | Shell command run once before any generator starts | +| `post_generate` | string | none | Shell command run once after every generator finishes | + +The C ABI symbol prefix is global by nature: every consumer must call the +identical exported symbols. The CLI resolves it once (`[global] c_prefix`, +then `[generators.c] prefix`) and fans it out to every per-target config +that leaves `prefix` unset, so a custom prefix is honored across all eleven +languages, not just C and C++. The generated C header aliases the runtime +symbols (`{prefix}_free_string`, `{prefix}_error_clear`, ...) back to the +canonical `weaveffi_*` names the `weaveffi-abi` runtime exports, so a Rust +producer needs no change. + +## `[generators.]` + +Every table also accepts a `prefix` key naming the C ABI symbol prefix its +wrappers call; you rarely set it per target because of the fan-out above. + +| Table | Key | Type | Default | Description | +|------------------------|-----------------------|--------|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `[generators.c]` | `prefix` | string | `"weaveffi"` | Prefix prepended to every C ABI symbol | +| `[generators.cpp]` | `namespace` | string | `"weaveffi"` | C++ namespace for the wrapper | +| `[generators.cpp]` | `header_name` | string | `"weaveffi.hpp"` | Header file name for the C++ output | +| `[generators.cpp]` | `standard` | string | `"17"` | C++ standard for the generated `CMakeLists.txt` | +| `[generators.swift]` | `module_name` | string | identity | Swift module name in `Package.swift` and the `Sources/` directory | +| `[generators.swift]` | `strip_module_prefix` | bool | `true` | Strip the module prefix from emitted Swift symbols | +| `[generators.android]` | `package` | string | `"com.weaveffi"` | Java/Kotlin package declaration in the JNI wrapper | +| `[generators.android]` | `strip_module_prefix` | bool | `true` | Strip the module prefix from emitted Kotlin symbols | +| `[generators.node]` | `package_name` | string | identity | npm package name | +| `[generators.node]` | `strip_module_prefix` | bool | `true` | Strip the module prefix from emitted JS/TS symbols | +| `[generators.wasm]` | `module_name` | string | `"weaveffi_wasm"` | Module name in the Wasm JS loader | +| `[generators.wasm]` | `emscripten` | bool | `false` | Target an Emscripten build: the loader accepts a pre-initialized Emscripten `Module` (or its `MODULARIZE` factory promise) instead of a `.wasm` URL; async, callbacks, and listeners become throwing stubs | +| `[generators.python]` | `package_name` | string | identity | Python package name | +| `[generators.python]` | `strip_module_prefix` | bool | `true` | Strip the module prefix from emitted Python symbols | +| `[generators.dotnet]` | `namespace` | string | identity | .NET namespace and NuGet package id | +| `[generators.dotnet]` | `strip_module_prefix` | bool | `true` | Strip the module prefix from emitted C# symbols | +| `[generators.dart]` | `package_name` | string | identity | Dart package name in `pubspec.yaml` | +| `[generators.dart]` | `strip_module_prefix` | bool | `true` | Strip the module prefix from emitted Dart symbols | +| `[generators.go]` | `module_path` | string | identity | Go module path in `go.mod` | +| `[generators.go]` | `strip_module_prefix` | bool | `true` | Strip the module prefix from emitted Go symbols | +| `[generators.ruby]` | `module_name` | string | `"WeaveFFI"` | Ruby module that wraps the bindings | +| `[generators.ruby]` | `gem_name` | string | identity | Ruby gem name | +| `[generators.ruby]` | `strip_module_prefix` | bool | `true` | Strip the module prefix from emitted Ruby symbols | + +"identity" means the value follows [Package identity](#package-identity). + +## Recipes ```toml -# iOS / macOS -[swift] -module_name = "MyAppFFI" +# iOS / macOS app with a branded module and symbol prefix +[global] +c_prefix = "myapp" -[c] -prefix = "myapp" +[generators.swift] +module_name = "MyAppFFI" ``` ```toml -# Android -[android] +# Android library +[generators.android] package = "com.example.myapp.ffi" - -[c] -prefix = "myapp" ``` ```toml -# Node -[node] +# Scoped npm package, module-prefixed names everywhere +[global] +strip_module_prefix = false + +[generators.node] package_name = "@myorg/myapp-native" ``` -The C ABI symbol prefix is global by nature: every consumer must call -the identical exported symbols. The CLI resolves it once -(`[global] c_prefix` wins, then `[c] prefix`) and fans it out to every -per-target config that hasn't set its own `prefix`, so a custom prefix -is honored across all eleven languages, not just C and C++. +```toml +# Build the producer before generating so `weaveffi package --build` can +# find fresh binaries +[global] +pre_generate = "cargo build --release" +``` -### 4. Wire it into CI +## Wiring it into CI -`weaveffi diff --check` enforces that the committed bindings still -match the IDL. A typical guard job: +`weaveffi diff --check` enforces that the committed bindings still match the +API definition and config. A typical guard job: ```yaml # .github/workflows/ci.yml - name: Verify generated bindings are up to date - run: weaveffi diff api.yml --out generated --check + run: weaveffi diff src/lib.rs --out generated --check ``` -`weaveffi validate --format json` and `weaveffi lint --format json` -are designed to be parsed by quality dashboards: +Exit codes: + +| Code | Meaning | +|------|------------------------------------------------------| +| `0` | The committed output matches the definition exactly. | +| `2` | One or more files would change in place. | +| `3` | One or more files would be added or removed. | + +`weaveffi validate --format json` emits structured success or failure, and +`--warn` adds the advisory lint warnings to the same document: ```bash -weaveffi --quiet validate api.yml --format json | jq '.ok' -weaveffi --quiet lint api.yml --format json > lint-report.json || \ - (cat lint-report.json && exit 1) +weaveffi --quiet validate src/lib.rs --warn --format json | jq '.ok, .warnings' +``` + +```json +{ "ok": true, "modules": 2, "functions": 8, "structs": 3, "enums": 1, "warnings": [] } ``` -## Reference - -TOML config files and inline IDL `generators:` blocks share the same -section names and key names. Pick the location that fits your workflow; -the keys are identical. - -### Per-target sections - -| Section | Key | Type | Default | Description | -|-------------|------------------------|--------|--------------------|-----------------------------------------------------------------------------| -| `[swift]` | `module_name` | string | `"WeaveFFI"` | Swift module name in `Package.swift` and the `Sources/` directory | -| `[swift]` | `strip_module_prefix` | bool | `true` | Strip the IR module prefix from emitted Swift symbols | -| `[android]` | `package` | string | `"com.weaveffi"` | Java/Kotlin package declaration in the JNI wrapper | -| `[android]` | `strip_module_prefix` | bool | `true` | Strip the IR module prefix from emitted Java/Kotlin symbols | -| `[node]` | `package_name` | string | `"weaveffi"` | npm package name in the Node.js loader | -| `[node]` | `strip_module_prefix` | bool | `true` | Strip the IR module prefix from emitted JS/TS symbols | -| `[wasm]` | `module_name` | string | `"weaveffi_wasm"` | Module name in the Wasm JS loader | -| `[wasm]` | `emscripten` | bool | `false` | Target an Emscripten build: the loader accepts a pre-initialized Emscripten `Module` (or its `MODULARIZE` factory promise) instead of a `.wasm` URL; async functions, callbacks, and listeners become throwing stubs | -| `[c]` | `prefix` | string | `"weaveffi"` | Prefix prepended to every C ABI symbol (`{prefix}_{module}_{function}`) | -| `[cpp]` | `namespace` | string | `"weaveffi"` | C++ namespace for the wrapper | -| `[cpp]` | `header_name` | string | `"weaveffi.hpp"` | Header file name for the C++ output | -| `[cpp]` | `standard` | string | `"17"` | C++ standard for the generated `CMakeLists.txt` | -| `[python]` | `package_name` | string | `"weaveffi"` | Python package name | -| `[python]` | `strip_module_prefix` | bool | `true` | Strip the IR module prefix from emitted Python symbols | -| `[dotnet]` | `namespace` | string | `"WeaveFFI"` | .NET namespace | -| `[dotnet]` | `strip_module_prefix` | bool | `true` | Strip the IR module prefix from emitted C# symbols | -| `[dart]` | `package_name` | string | `"weaveffi"` | Dart package name in `pubspec.yaml` | -| `[dart]` | `strip_module_prefix` | bool | `true` | Strip the IR module prefix from emitted Dart symbols | -| `[go]` | `module_path` | string | `"weaveffi"` | Go module path in `go.mod` | -| `[go]` | `strip_module_prefix` | bool | `true` | Strip the IR module prefix from emitted Go symbols | -| `[ruby]` | `module_name` | string | `"WeaveFFI"` | Ruby module that wraps the bindings | -| `[ruby]` | `gem_name` | string | `"weaveffi"` | Ruby gem name | -| `[ruby]` | `strip_module_prefix` | bool | `true` | Strip the IR module prefix from emitted Ruby symbols | - -Every per-target section also accepts a `prefix` key naming the C ABI -symbol prefix its wrappers call. You rarely set it per target: the CLI -fans the resolved global prefix (`[global] c_prefix`, or `[c] prefix`) -out to every section that leaves it unset, so all eleven targets call -the same exported symbols. - -> **Package identity.** The name, version, and metadata stamped into every -> generated manifest are resolved from the IDL -> [`package:` block](../reference/idl.md#package-metadata) by one shared -> policy. For an identity value an explicit key below wins; otherwise it falls -> back to the `package:` name (normalized per ecosystem), then the IDL file -> stem, then the `"weaveffi"`/`"WeaveFFI"` default shown above. The keys that -> participate are `[swift] module_name`, `[node] package_name`, -> `[python] package_name`, `[dart] package_name`, `[go] module_path`, -> `[ruby] gem_name`, and `[dotnet] namespace` (which also sets the NuGet -> package id). Manifests with no dedicated key (Android `rootProject.name`, -> the Wasm `package.json`, and the C++ `CMakeLists.txt` version) follow the -> same identity, and the published version comes from `package.version` -> (default `0.1.0`). All other keys (e.g. `[c] prefix`, `[cpp] namespace`, -> `[android] package`, `[ruby] module_name`, `[wasm] module_name`) keep the -> fixed defaults above. - -### `[global]` section - -| Key | Type | Default | Description | -|------------------------|--------|--------------------|-----------------------------------------------------------------------------| -| `strip_module_prefix` | bool | _unset_ | Shorthand: sets `strip_module_prefix` on every target that supports it, overriding their sections. Stripping is on by default, so `false` restores module-prefixed names everywhere at once | -| `c_prefix` | string | _unset_ | Global C ABI symbol prefix, fanned out to every per-target `prefix` that is unset; wins over `[c] prefix` as the resolution source | -| `pre_generate` | string | _none_ | Shell command run before any generator starts | -| `post_generate` | string | _none_ | Shell command run after every generator finishes | - -The alias `[weaveffi]` is accepted for the `[global]` section. - -### Performance and CI flags - -- The orchestrator dispatches every selected generator in parallel - using [rayon](https://docs.rs/rayon). The pre- and post-generate - hooks still run serially around the whole batch. +```json +{ + "ok": false, + "errors": [ + { + "code": "DuplicateFunctionName", + "module": "math", + "function": "add", + "message": "duplicate function name in module 'math': add", + "suggestion": "function names must be unique within a module; rename the duplicate" + } + ] +} +``` + +Warnings never change `ok` or the exit status; they carry stable `code`, +`location`, and `message` fields for dashboards to key on. + +## Performance and caching + +- The orchestrator dispatches every selected generator in parallel using + [rayon](https://docs.rs/rayon). The `pre_generate` and `post_generate` + hooks run serially around the whole batch. - Each generator persists a hash under - `{out_dir}/.weaveffi-cache/{target}.hash`. Only generators whose - hash changed are re-run; pass `--force` to invalidate every entry. -- `weaveffi diff --check` exit codes: - - | Code | Meaning | - |------|---------| - | `0` | The committed output matches the IDL exactly. | - | `2` | One or more files would change in place. | - | `3` | One or more files would be added or removed. | - -- `weaveffi validate --format json` emits structured success/failure: - - ```json - { "ok": true, "modules": 2, "functions": 8, "structs": 3, "enums": 1 } - ``` - - ```json - { - "ok": false, - "errors": [ - { - "code": "DuplicateFunctionName", - "module": "math", - "function": "add", - "message": "duplicate function name in module 'math': add", - "suggestion": "function names must be unique within a module; rename the duplicate" - } - ] - } - ``` - -- `weaveffi lint --format json` returns the warning list with stable - `code` / `location` / `message` fields: - - ```json - { - "ok": false, - "warnings": [ - { - "code": "DeepNesting", - "location": "math::compute::matrix", - "message": "deep type nesting at math::compute::matrix (depth 4, max recommended 3)" - } - ] - } - ``` + `{out_dir}/.weaveffi-cache/{target}.hash` covering the resolved API + (including `[package]`), the generator's name and config, and the CLI + version. Only generators whose hash changed re-run; pass `--force` to + invalidate every entry. ## Pitfalls -- **Inline value overrides TOML silently**: there is no warning when - both are set. If a TOML override "doesn't take", check for an inline - block in the IDL. - **The C prefix rewrites every generator**: picking a custom prefix - also rewrites the runtime symbols (`{prefix}_free_string`, ...). The - Rust cdylib must be built with the same prefix. Every wrapper picks - it up automatically from the resolved global value; if you also set - a per-target `prefix`, make sure they agree. -- **Module-prefix stripping flattens names**: it's on by default, so - two modules that each declare an `open` function collide in targets - with a flat namespace. Rename one, or set - `strip_module_prefix = false` (globally or per target) to restore - prefixed names. -- **Hooks run shell commands as-is**: `pre_generate` and - `post_generate` are passed straight to `sh -c`. Quote them - carefully and never include untrusted input. -- **Cache covers IR, generator name, generator config, and CLI version**: - changing the IR, any generator config field, or upgrading the CLI - invalidates the per-generator cache and triggers re-emission. -- **Older CLIs ignore unknown keys**: adding a new generator key - with a project-wide implication does not error out on older - toolchains. Pin the CLI version in CI when you need that guarantee. + renames every exported business symbol. Rust producers using + `#[weaveffi::module]` must be built with the same prefix; every wrapper + picks it up from the resolved global value, so if you also set a + per-target `prefix`, make sure they agree. +- **Module-prefix stripping flattens names**: it's on by default, so two + modules that each declare an `open` function collide in targets with a + flat namespace. Rename one, or set `strip_module_prefix = false` + (globally or per target) to restore prefixed names. +- **Hooks run shell commands as-is**: `pre_generate` and `post_generate` + are passed straight to `sh -c`. Quote them carefully and never include + untrusted input. +- **Config lives outside the definition**: two checkouts generating from + the same `lib.rs` with different `weaveffi.toml` files produce different + manifests. Commit the config next to the definition and let discovery + find it rather than passing `--config` from scripts. diff --git a/docs/src/guides/errors.md b/docs/src/guides/errors.md index 29169f14..a2fa27b4 100644 --- a/docs/src/guides/errors.md +++ b/docs/src/guides/errors.md @@ -41,7 +41,7 @@ Reach for this guide when: ### Declare a domain and opt in with `throws` ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: contacts errors: diff --git a/docs/src/guides/extract.md b/docs/src/guides/extract.md index bc5afa38..72b8881c 100644 --- a/docs/src/guides/extract.md +++ b/docs/src/guides/extract.md @@ -236,9 +236,9 @@ The extractor parses syntax, not semantics. The items below cannot be inferred from Rust source alone and either must be added to the generated IDL by hand or are documented as round-trip gaps. -- **Package and per-generator configuration.** The `package:` and - `generators:` blocks have no source-level spelling; add them to the - extracted IDL by hand (this is why the advanced samples commit a YAML). +- **Package and per-generator configuration.** These aren't part of the + API definition at all; they live in the `weaveffi.toml` beside the crate + and apply equally to extracted and hand-written IDLs. - **Struct field default values.** The IDL's `default:` field cannot be derived from Rust syntax (Rust struct fields have no default expressions). - **Standalone `since:` without `#[deprecated]`.** `since` is only recovered diff --git a/docs/src/guides/packaging.md b/docs/src/guides/packaging.md index b476a7b0..73fa85b4 100644 --- a/docs/src/guides/packaging.md +++ b/docs/src/guides/packaging.md @@ -26,10 +26,8 @@ gets them one of two ways: rustup target and a working cross-linker installed (`rustup target add aarch64-unknown-linux-gnu`, and so on). -The two are mutually exclusive. Before a `--build` run, `weaveffi doctor --target -package` reports which producer cross-targets are installed (and the -`rustup target add` command for any that are missing), and exits non-zero if any -are absent so it can gate the build in CI. +The two are mutually exclusive. A `--build` run fails with the missing +`rustup target add` command when a platform's Rust target isn't installed. ### The `--binaries` layout diff --git a/docs/src/performance.md b/docs/src/performance.md index 14b8e0b0..12b5a638 100644 --- a/docs/src/performance.md +++ b/docs/src/performance.md @@ -20,9 +20,9 @@ measure the full code-generation surface (all 11 generators) end-to-end. | Benchmark | Target | Inputs | | --------------------------- | ------- | ------------------------------------------------------- | -| `validate_kitchen_sink` | < 5 ms | `crates/weaveffi-cli/tests/fixtures/06_kitchen_sink.yml` | +| `validate_kitchen_sink` | < 5 ms | `crates/weaveffi-cli/tests/fixtures/kitchen_sink.yml` | | `hash_kitchen_sink` | < 1 ms | Same fixture, post-validation | -| `full_codegen_calculator` | < 500 ms| `samples/calculator/calculator.yml`, all 11 generators | +| `full_codegen_calculator` | < 500 ms| `samples/calculator/src/lib.rs`, all 11 generators | | `full_codegen_kitchen_sink` | < 2000 ms | Kitchen-sink fixture, all 11 generators | A regression that pushes any of these benchmarks past its target is a diff --git a/docs/src/reference/idl.md b/docs/src/reference/idl.md index 2be3de06..577b86dd 100644 --- a/docs/src/reference/idl.md +++ b/docs/src/reference/idl.md @@ -26,12 +26,10 @@ and objects: ```text # yaml-language-server: $schema=./weaveffi.schema.json -version: "0.7.0" -package: - name: my_app - version: "1.0.0" +version: "0.8.0" modules: - name: my_module + doc: "..." structs: [...] enums: [...] interfaces: [...] @@ -40,20 +38,21 @@ modules: listeners: [...] errors: { ... } modules: [...] -generators: - swift: - module_name: MyApp ``` +The document describes only the API. Package identity and per-generator +options live in a `weaveffi.toml` next to it; see +[Project Configuration](../guides/config.md). Unknown top-level keys are +rejected, so a stray `package:` or `generators:` block fails validation +instead of being silently ignored. + A complete, validating example lives at the bottom of this page in the [Complete example](#complete-example) section. | Field | Type | Required | Description | |--------------|-----------------------------|----------|------------------------------------------| -| `version` | string | yes | Schema version; only the current version (`"0.7.0"`) is accepted | -| `package` | Package | no | Publishable identity stamped into every generated manifest (see [Package metadata](#package-metadata)) | +| `version` | string | yes | Schema version; only the current version (`"0.8.0"`) is accepted | | `modules` | array of Module | yes | One or more modules | -| `generators` | map of string to object | no | Per-generator configuration (see [generators section](#generators-section)) | ### Module @@ -96,72 +95,25 @@ A complete, validating example lives at the bottom of this page in the ## Package metadata -The optional top-level `package` block is the single source of truth for the -publishable identity stamped into every generated ecosystem manifest: -`package.json` (Node/Wasm), `pyproject.toml`/`setup.py` (Python), -`*.gemspec` (Ruby), `*.csproj`/`*.nuspec` (.NET), `pubspec.yaml` (Dart), -`Package.swift` (Swift), `go.mod` (Go), `settings.gradle` (Android), and -`CMakeLists.txt` (C++). Declaring it once keeps the name, version, and -metadata consistent across all eleven targets instead of every generator -hardcoding `weaveffi` / `0.1.0`. - -### Package schema - -| Field | Type | Required | Description | -|---------------|-----------------|----------|-----------------------------------------------------| -| `name` | string | yes | Distribution name (npm/PyPI/gem/NuGet/pub/...) | -| `version` | string | yes | Semantic version stamped into each manifest | -| `description` | string | no | One-line package description | -| `license` | string | no | SPDX license expression (e.g. `MIT`, `Apache-2.0`) | -| `authors` | array of string | no | Author entries (`Name `) | -| `homepage` | string | no | Project homepage URL | -| `repository` | string | no | Source repository URL | - -### Name and version resolution - -Each target resolves its package name with the following precedence (first -non-empty wins): - -1. an explicit per-target override (e.g. `python.package_name`, - `dart.package_name`, `ruby.gem_name`), -2. `package.name`, -3. the IDL file stem (e.g. `kvstore.yml` → `kvstore`), -4. the built-in default `weaveffi`. - -The version resolves from `package.version`, falling back to `0.1.0`. Names -are normalized per ecosystem, e.g. a Python import package or Ruby `require` -path lowercases and replaces non-alphanumerics with `_` (`my-kv.store` → -`my_kv_store`), while the published distribution name keeps the original -spelling. - -> Code-level identity that has no manifest of its own still follows the -> package where it is unambiguous: the Swift module name defaults to the -> PascalCased `package.name` (`async-demo` → `AsyncDemo`). The stable C ABI -> symbol prefix is **not** affected: it stays `weaveffi` (or your global -> `c_prefix`) so the generated bindings keep calling the symbols the producer -> exports. - -### Package example - -```yaml -version: "0.7.0" -package: - name: kvstore - version: "1.0.0" - description: An embedded key-value store API. - license: MIT - authors: - - WeaveFoundry - homepage: https://github.com/weavefoundry/weaveffi - repository: https://github.com/weavefoundry/weaveffi -modules: - - name: kv - functions: - - name: count - params: [] - return: i64 +The IDL carries no package metadata. The name, version, and other +distribution identity stamped into every generated ecosystem manifest +(`package.json`, `pyproject.toml`, `*.gemspec`, `*.csproj`, `pubspec.yaml`, +`Package.swift`, `go.mod`, `settings.gradle`, `CMakeLists.txt`) come from the +`[package]` table of the `weaveffi.toml` beside the definition: + +```toml +[package] +name = "kvstore" +version = "1.0.0" +description = "An embedded key-value store API." +license = "MIT" +authors = ["WeaveFoundry "] +repository = "https://github.com/weavefoundry/weaveffi" ``` +See [Project Configuration](../guides/config.md#package) for the schema and +the per-ecosystem name resolution rules. + --- ## Primitive types @@ -196,7 +148,7 @@ parameters and return types. ### Primitive examples ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: primitives structs: @@ -276,7 +228,7 @@ knows how to spell the handle's type. At the C ABI level, `handle` is still a `uint64_t`. ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: sessions structs: @@ -330,7 +282,7 @@ Each field: ### Struct example ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: geometry structs: @@ -414,7 +366,7 @@ Each variant: ### Enum example ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: contacts enums: @@ -448,7 +400,7 @@ Swift `enum` with associated values). A *unit* variant (no `fields`) and a *data variant may coexist in the same enum. ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: shapes enums: @@ -560,7 +512,7 @@ positions. A trimmed version of the `kvstore` sample's `Store` interface: ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: kv errors: @@ -650,7 +602,7 @@ the default is null. ### Optional example ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: contacts structs: @@ -692,7 +644,7 @@ Wrap a type in `[T]` brackets to declare a list (variable-length sequence). ### List example ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: lists structs: @@ -737,7 +689,7 @@ valid `TypeRef`. ### Map example ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: maps structs: @@ -821,7 +773,7 @@ because every composite serializes into one value buffer: ### Nested type example ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: nested structs: @@ -868,7 +820,7 @@ one at a time and are suitable for large or streaming result sets. ### Iterator example ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: streaming structs: @@ -922,7 +874,7 @@ invokes a caller-provided function. ### Callback example ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: events functions: [] @@ -962,7 +914,7 @@ with subscribe/unsubscribe lifecycle management. ### Listener example ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: events functions: [] @@ -990,7 +942,7 @@ modules. ### Nested module example ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: app functions: @@ -1039,7 +991,7 @@ For example, a nested `stats` module can take the parent `kv` module's `Store` interface as a parameter: ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: kv errors: @@ -1086,7 +1038,7 @@ Functions can be marked as asynchronous. See the behaviour. ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: net errors: @@ -1125,7 +1077,7 @@ return a list instead or make the function synchronous. Mark a function as deprecated with a migration message: ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: legacy functions: @@ -1147,7 +1099,7 @@ Generators propagate the deprecation message to the target language Mark a parameter as mutable when the callee may modify it in-place: ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: buffers functions: @@ -1161,38 +1113,24 @@ generated wrapper code in target languages. --- -## Generators section +## Generator options -The top-level `generators` key provides per-generator configuration -directly in the IDL file. This is an alternative to using a separate -TOML configuration file with `--config`. +Per-generator options (Swift module name, Android package, Go module path, +and so on) are not part of the IDL. They live under `[generators.]` +tables in `weaveffi.toml`, where `` matches the `--target` flag: -```yaml -version: "0.7.0" -modules: - - name: math - functions: - - name: add - params: - - { name: a, type: i32 } - - { name: b, type: i32 } - return: i32 +```toml +[generators.swift] +module_name = "MyMathLib" + +[generators.android] +package = "com.example.math" -generators: - swift: - module_name: MyMathLib - android: - package: com.example.math - ruby: - module_name: MathBindings - gem_name: math_bindings - go: - module_path: github.com/myorg/mathlib +[generators.go] +module_path = "github.com/myorg/mathlib" ``` -Each key under `generators` is the target name (matching the `--target` -flag). The value is a target-specific configuration object. See the -[Generator Configuration guide](../guides/config.md) for the full list +See the [Project Configuration guide](../guides/config.md) for the full list of options. --- @@ -1239,7 +1177,7 @@ A full IDL combining structs, enums, optionals, lists, an interface, and a typed error domain (a trimmed version of the `contacts` sample): ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: contacts enums: @@ -1422,7 +1360,7 @@ Declaring a domain reserves the codes; a function, method, or constructor joins the typed error path by declaring `throws: true`: ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: contacts errors: @@ -1507,7 +1445,7 @@ Per-target syntax: Example IDL: ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: docs structs: diff --git a/docs/src/samples.md b/docs/src/samples.md index 3f2e4309..ae1b8d7b 100644 --- a/docs/src/samples.md +++ b/docs/src/samples.md @@ -3,13 +3,11 @@ This repo includes sample projects under `samples/` that showcase end-to-end usage of WeaveFFI. Every producer is written as safe Rust and annotated with the `#[weaveffi::module]` family of attributes, so the macro generates its C ABI -(see [The Rust Producer Macro](guides/producer-macro.md)). The simpler producers -(`calculator`, `contacts`, and `inventory`) generate bindings straight from -their annotated source. The advanced samples (`async-demo`, `events`, `kvstore`, -`shapes`) are macro-annotated too, and they keep a committed YAML IDL as the -generation source of truth because their surfaces carry metadata the extractor -does not yet recover from source, such as package and per-generator -configuration and standalone `since` tags. +(see [The Rust Producer Macro](guides/producer-macro.md)), and every sample +generates its bindings straight from that annotated `src/lib.rs`: there is no +parallel IDL file to keep in sync. Package identity and per-target options +live in each sample's `weaveffi.toml`, which the CLI discovers automatically +next to the crate. ## Kvstore (kitchen-sink reference) @@ -38,16 +36,16 @@ real-world pattern for a new generator. - A deprecated method (`legacy_put`) - A nested sub-module (`kv.stats`) with its own struct (`Stats`) and a function that takes a cross-module `Store` parameter -- Inline `generators:` overrides for `swift.module_name`, - `cpp.namespace`, `dotnet.namespace`, `dart.package_name`, - `go.module_path`, and `ruby.module_name` +- A `weaveffi.toml` with `[generators.]` overrides for + `swift.module_name`, `cpp.namespace`, `dotnet.namespace`, + `dart.package_name`, `go.module_path`, and `ruby.module_name` **Build, generate bindings, and run the C ABI tests:** ```bash cargo build -p kvstore cargo test -p kvstore -weaveffi generate samples/kvstore/kvstore.yml -o generated +weaveffi generate samples/kvstore/src/lib.rs -o generated ``` The `conformance/` harness ships a kvstore consumer for every language that @@ -81,7 +79,7 @@ how each backend surfaces it as an idiomatic sum type. ```bash cargo build -p shapes cargo test -p shapes -weaveffi generate samples/shapes/shapes.yml -o generated +weaveffi generate samples/shapes/src/lib.rs -o generated ``` The `conformance/` harness ships a `shapes` consumer for every language that diff --git a/docs/src/stability.md b/docs/src/stability.md index ea12cb75..b598d0fd 100644 --- a/docs/src/stability.md +++ b/docs/src/stability.md @@ -28,9 +28,16 @@ After the 1.0.0 release, the following surfaces will be governed by SemVer: `weaveffi-gen-swift`, `weaveffi-gen-android`, `weaveffi-gen-node`, `weaveffi-gen-wasm`, `weaveffi-gen-python`, `weaveffi-gen-dotnet`, `weaveffi-gen-dart`, `weaveffi-gen-go`, `weaveffi-gen-ruby`, and - `weaveffi-cli`. The `Generator` trait, the `Orchestrator`, the IR types, - and the C ABI runtime symbols exported from `weaveffi-abi` are all public - contracts. + `weaveffi-cli`. The `LanguageBackend` trait, the `Orchestrator`, the IR + types, and the C ABI runtime symbols exported from `weaveffi-abi` are all + public contracts. +- **The C ABI revision.** The `weaveffi_abi_version()` runtime symbol + reports the revision of the runtime surface (the `weaveffi_error` layout, + the value-buffer encoding, and the `weaveffi_*` symbol set). Generated + consumers embed the revision they were built for and, where a load-time + check is cheap (Python, Ruby, Dart, Go, .NET, Node.js, Wasm), refuse to + run against a producer reporting a different one. The revision only + changes with a major release. ## What is NOT covered pre-1.0 @@ -67,9 +74,19 @@ contract." Things that have already changed during 0.x: `*_with_config` method pair. A prototype Tera template hook (`generate_with_templates`, `--templates`, `template_dir`) was added and then removed in `0.4.0` because no generator ever consumed it. + Schema `0.8.0` folded `Generator` into the single `LanguageBackend` + trait (`Target` is its object-safe view), and generators now consume a + fully resolved `Ty` type model rather than matching on the IDL's + `TypeRef` strings. - The C ABI runtime added `weaveffi_arena_*` and `weaveffi_cancel_token_*` - families. -- `weaveffi doctor` gained `--target` and `--format json`. + families, and then `weaveffi_abi_version`. +- Schema `0.8.0` removed the top-level `package:` and `generators:` blocks + from the IDL. Package identity and per-target options moved to a + `weaveffi.toml` next to the definition (auto-discovered, or named with + `--config`), and unknown top-level IDL keys are now rejected. The + peripheral `new`, `scaffold`, `watch`, `format`, `lint`, `doctor`, and + `man` CLI commands were removed; `validate --warn` reports the former + lint warnings. Pin the WeaveFFI version in CI (`cargo install weaveffi-cli --version =0.3.0`) and vendor the generated output in your repository so that @@ -103,7 +120,7 @@ tied to `weaveffi-ir`'s minor version: each `weaveffi-ir` minor bump corresponds to at most one schema version bump. [`CURRENT_SCHEMA_VERSION`](https://github.com/weavefoundry/weaveffi/blob/main/crates/weaveffi-ir/src/ir.rs) in `crates/weaveffi-ir/src/ir.rs` is the source of truth; the current -schema version is `0.7.0`. +schema version is `0.8.0`. Pre-1.0, **only the current schema version is accepted** (`SUPPORTED_VERSIONS` contains exactly `CURRENT_SCHEMA_VERSION`), so a @@ -166,9 +183,8 @@ A typical GitHub Actions step: weaveffi diff idl/api.yml --out generated/ --check ``` -Combine it with `weaveffi format --check idl/api.yml` (canonical IDL) and -`weaveffi validate idl/api.yml` (schema correctness) for a complete CI -guard. +Combine it with `weaveffi validate --warn idl/api.yml` (schema correctness +plus advisory lints) for a complete CI guard. ## See also diff --git a/docs/src/tutorials/android.md b/docs/src/tutorials/android.md index 7071ba2f..9b57039b 100644 --- a/docs/src/tutorials/android.md +++ b/docs/src/tutorials/android.md @@ -24,7 +24,7 @@ emulator or a physical device. Save as `greeter.yml`: ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: greeter errors: @@ -56,7 +56,7 @@ domain when the language is unknown. ### 2. Generate bindings ```bash -weaveffi generate greeter.yml -o generated --scaffold +weaveffi generate greeter.yml -o generated ``` You should see, among other targets: @@ -65,15 +65,14 @@ You should see, among other targets: generated/ ├── c/ │ └── weaveffi.h -├── android/ -│ ├── settings.gradle -│ ├── build.gradle -│ └── src/main/ -│ ├── kotlin/com/weaveffi/WeaveFFI.kt -│ └── cpp/ -│ ├── weaveffi_jni.c -│ └── CMakeLists.txt -└── scaffold.rs +└── android/ + ├── settings.gradle + ├── build.gradle + └── src/main/ + ├── kotlin/com/weaveffi/WeaveFFI.kt + └── cpp/ + ├── weaveffi_jni.c + └── CMakeLists.txt ``` ### 3. Implement the Rust library @@ -118,13 +117,17 @@ pub extern "C" fn weaveffi_greeter_hello( CString::new(msg).unwrap().into_raw() as *const c_char } -// Emit the WeaveFFI C ABI runtime symbols (free_string, free_bytes, -// error_clear, cancel_token_*), one line per cdylib. +// Emit the WeaveFFI C ABI runtime symbols (abi_version, free_string, +// free_bytes, error_clear, cancel_token_*), one line per cdylib. abi::export_runtime!(); ``` -Use `scaffold.rs` for the rest of the API (`weaveffi_greeter_greeting`, -which returns the `Greeting` record as a serialized value buffer). +The generated `generated/c/weaveffi.h` lists every remaining symbol with +its exact signature; implement `weaveffi_greeter_greeting` the same way +(it returns the `Greeting` record as a serialized value buffer). +Alternatively, annotate the module with `#[weaveffi::module]` and let the +macro emit the whole C ABI; see +[The Rust Producer Macro](../guides/producer-macro.md). ### 4. Configure the NDK toolchain diff --git a/docs/src/tutorials/calculator.md b/docs/src/tutorials/calculator.md index b8f85575..3f68efef 100644 --- a/docs/src/tutorials/calculator.md +++ b/docs/src/tutorials/calculator.md @@ -25,8 +25,8 @@ by zero. ### 1. Generate every target -Point the generator at the annotated source (the `calculator.yml` IDL still -works too, and produces the same bindings): +Point the generator at the annotated source; the sample's `weaveffi.toml` +beside it supplies the package identity: ```bash weaveffi generate samples/calculator/src/lib.rs -o generated diff --git a/docs/src/tutorials/node.md b/docs/src/tutorials/node.md index a17274af..414d705b 100644 --- a/docs/src/tutorials/node.md +++ b/docs/src/tutorials/node.md @@ -22,7 +22,7 @@ package shape ready to publish. Save as `greeter.yml`: ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: greeter errors: @@ -54,7 +54,7 @@ domain when the language is unknown. ### 2. Generate bindings ```bash -weaveffi generate greeter.yml -o generated --scaffold +weaveffi generate greeter.yml -o generated ``` Among other targets you should see: @@ -63,13 +63,12 @@ Among other targets you should see: generated/ ├── c/ │ └── weaveffi.h -├── node/ -│ ├── binding.gyp -│ ├── index.js -│ ├── package.json -│ ├── types.d.ts -│ └── weaveffi_addon.c -└── scaffold.rs +└── node/ + ├── binding.gyp + ├── index.js + ├── package.json + ├── types.d.ts + └── weaveffi_addon.c ``` `weaveffi_addon.c` is a complete N-API addon that bridges Node's @@ -118,13 +117,17 @@ pub extern "C" fn weaveffi_greeter_hello( CString::new(msg).unwrap().into_raw() as *const c_char } -// Emit the WeaveFFI C ABI runtime symbols (free_string, free_bytes, -// error_clear, cancel_token_*), one line per cdylib. +// Emit the WeaveFFI C ABI runtime symbols (abi_version, free_string, +// free_bytes, error_clear, cancel_token_*), one line per cdylib. abi::export_runtime!(); ``` -Use `scaffold.rs` for the rest of the API; it lists every symbol the -addon expects, with exact signatures. +The generated `generated/c/weaveffi.h` lists every remaining symbol the +addon expects, with exact signatures; implement `weaveffi_greeter_greeting` +the same way (it returns the `Greeting` record as a serialized value +buffer). Alternatively, annotate the module with `#[weaveffi::module]` and +let the macro emit the whole C ABI; see +[The Rust Producer Macro](../guides/producer-macro.md). ### 4. Build the cdylib and the N-API addon diff --git a/docs/src/tutorials/python.md b/docs/src/tutorials/python.md index de9e944c..c73d9ee5 100644 --- a/docs/src/tutorials/python.md +++ b/docs/src/tutorials/python.md @@ -20,7 +20,7 @@ script. Save as `greeter.yml`: ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: greeter errors: @@ -52,7 +52,7 @@ domain when the language is unknown. ### 2. Generate bindings ```bash -weaveffi generate greeter.yml -o generated --scaffold +weaveffi generate greeter.yml -o generated ``` Among other targets, you should see: @@ -61,20 +61,20 @@ Among other targets, you should see: generated/ ├── c/ │ └── weaveffi.h -├── python/ -│ ├── pyproject.toml -│ ├── setup.py -│ ├── README.md -│ └── greeter/ -│ ├── __init__.py -│ ├── weaveffi.py -│ └── weaveffi.pyi -└── scaffold.rs +└── python/ + ├── pyproject.toml + ├── setup.py + ├── README.md + └── greeter/ + ├── __init__.py + ├── weaveffi.py + └── weaveffi.pyi ``` -The package directory and distribution name follow the IDL package name -(here `greeter`). The Python target uses ctypes: no native extension to -compile on the Python side. +The package directory and distribution name follow the `[package]` name +from `weaveffi.toml`, falling back to the IDL file stem (here `greeter`). +The Python target uses ctypes: no native extension to compile on the Python +side. ### 3. Implement the Rust library @@ -118,13 +118,17 @@ pub extern "C" fn weaveffi_greeter_hello( CString::new(msg).unwrap().into_raw() as *const c_char } -// Emit the WeaveFFI C ABI runtime symbols (free_string, free_bytes, -// error_clear, cancel_token_*), one line per cdylib. +// Emit the WeaveFFI C ABI runtime symbols (abi_version, free_string, +// free_bytes, error_clear, cancel_token_*), one line per cdylib. abi::export_runtime!(); ``` -Use `scaffold.rs` for the rest of the API; it lists every symbol the -bindings expect, with exact signatures. +The generated `generated/c/weaveffi.h` lists every remaining symbol the +bindings expect, with exact signatures; implement `weaveffi_greeter_greeting` +the same way (it returns the `Greeting` record as a serialized value +buffer). Alternatively, annotate the module with `#[weaveffi::module]` and +let the macro emit the whole C ABI; see +[The Rust Producer Macro](../guides/producer-macro.md). ### 4. Build the cdylib diff --git a/docs/src/tutorials/swift.md b/docs/src/tutorials/swift.md index 9543beb0..ebb364fa 100644 --- a/docs/src/tutorials/swift.md +++ b/docs/src/tutorials/swift.md @@ -24,7 +24,7 @@ simulator. Save as `greeter.yml`: ```yaml -version: "0.7.0" +version: "0.8.0" modules: - name: greeter errors: @@ -56,7 +56,7 @@ domain when the language is unknown. ### 2. Generate bindings ```bash -weaveffi generate greeter.yml -o generated --scaffold +weaveffi generate greeter.yml -o generated ``` You should see, among other targets: @@ -65,14 +65,13 @@ You should see, among other targets: generated/ ├── c/ │ └── weaveffi.h -├── swift/ -│ ├── Package.swift -│ └── Sources/ -│ ├── CWeaveFFI/ -│ │ └── module.modulemap -│ └── WeaveFFI/ -│ └── WeaveFFI.swift -└── scaffold.rs +└── swift/ + ├── Package.swift + └── Sources/ + ├── CWeaveFFI/ + │ └── module.modulemap + └── WeaveFFI/ + └── WeaveFFI.swift ``` ### 3. Implement the Rust library @@ -117,14 +116,17 @@ pub extern "C" fn weaveffi_greeter_hello( CString::new(msg).unwrap().into_raw() as *const c_char } -// Emit the WeaveFFI C ABI runtime symbols (free_string, free_bytes, -// error_clear, cancel_token_*), one line per cdylib. +// Emit the WeaveFFI C ABI runtime symbols (abi_version, free_string, +// free_bytes, error_clear, cancel_token_*), one line per cdylib. abi::export_runtime!(); ``` -Use `scaffold.rs` as the template for the rest of the API -(`weaveffi_greeter_greeting`, which returns the `Greeting` record as a -serialized value buffer). +The generated `generated/c/weaveffi.h` lists every remaining symbol with +its exact signature; implement `weaveffi_greeter_greeting` the same way +(it returns the `Greeting` record as a serialized value buffer). +Alternatively, annotate the module with `#[weaveffi::module]` and let the +macro emit the whole C ABI; see +[The Rust Producer Macro](../guides/producer-macro.md). ### 4. Build for iOS targets diff --git a/justfile b/justfile index 7c25f769..297c593d 100644 --- a/justfile +++ b/justfile @@ -4,7 +4,7 @@ build: # Generate bindings from the calculator sample generate: build - cargo run -p weaveffi-cli -- generate samples/calculator/calculator.yml -o generated + cargo run -p weaveffi-cli -- generate samples/calculator/src/lib.rs -o generated # Run all tests test: diff --git a/samples/async-demo/async_demo.yml b/samples/async-demo/async_demo.yml deleted file mode 100644 index b8dd5239..00000000 --- a/samples/async-demo/async_demo.yml +++ /dev/null @@ -1,46 +0,0 @@ -version: "0.7.0" -package: - name: async-demo - version: "1.0.0" - description: Async/await and cancellation demo across WeaveFFI's async-capable targets. - license: MIT - authors: - - WeaveFoundry - homepage: https://github.com/weavefoundry/weaveffi - repository: https://github.com/weavefoundry/weaveffi -modules: - - name: tasks - structs: - - name: TaskResult - fields: - - { name: id, type: i64 } - - { name: value, type: string } - - { name: success, type: bool } - errors: - name: TaskError - codes: - - { name: InvalidName, code: 1, message: "task name must not be empty" } - functions: - - name: run_task - params: - - { name: name, type: string } - return: TaskResult - async: true - throws: true - - name: run_batch - params: - - { name: names, type: "[string]" } - return: "[TaskResult]" - async: true - - name: cancel_task - params: - - { name: id, type: i64 } - return: bool - - name: run_n_tasks - params: - - { name: n, type: i32 } - return: i32 - async: true - - name: active_callbacks - params: [] - return: i64 diff --git a/samples/async-demo/weaveffi.toml b/samples/async-demo/weaveffi.toml new file mode 100644 index 00000000..3f31d3c6 --- /dev/null +++ b/samples/async-demo/weaveffi.toml @@ -0,0 +1,8 @@ +[package] +name = "async-demo" +version = "1.0.0" +description = "Async/await and cancellation demo across WeaveFFI's async-capable targets." +license = "MIT" +authors = ["WeaveFoundry "] +homepage = "https://github.com/weavefoundry/weaveffi" +repository = "https://github.com/weavefoundry/weaveffi" diff --git a/samples/calculator/calculator.yml b/samples/calculator/calculator.yml deleted file mode 100644 index bf7eac52..00000000 --- a/samples/calculator/calculator.yml +++ /dev/null @@ -1,41 +0,0 @@ -version: "0.7.0" -package: - name: calculator - version: "1.0.0" - description: Arithmetic helpers exposed across every WeaveFFI target language. - license: MIT - authors: - - WeaveFoundry - homepage: https://github.com/weavefoundry/weaveffi - repository: https://github.com/weavefoundry/weaveffi -modules: - - name: calculator - errors: - name: CalcError - codes: - - { name: DivisionByZero, code: 1, message: "division by zero" } - functions: - - name: add - doc: Add two integers - params: - - { name: a, type: i32 } - - { name: b, type: i32 } - return: i32 - - name: mul - doc: Multiply two integers - params: - - { name: a, type: i32 } - - { name: b, type: i32 } - return: i32 - - name: div - doc: Divide two integers, failing on a zero divisor - params: - - { name: a, type: i32 } - - { name: b, type: i32 } - return: i32 - throws: true - - name: echo - doc: Echo a string back - params: - - { name: s, type: string } - return: string diff --git a/samples/calculator/weaveffi.toml b/samples/calculator/weaveffi.toml new file mode 100644 index 00000000..c570522c --- /dev/null +++ b/samples/calculator/weaveffi.toml @@ -0,0 +1,8 @@ +[package] +name = "calculator" +version = "1.0.0" +description = "Arithmetic helpers exposed across every WeaveFFI target language." +license = "MIT" +authors = ["WeaveFoundry "] +homepage = "https://github.com/weavefoundry/weaveffi" +repository = "https://github.com/weavefoundry/weaveffi" diff --git a/samples/contacts/contacts.yml b/samples/contacts/contacts.yml deleted file mode 100644 index f7236e77..00000000 --- a/samples/contacts/contacts.yml +++ /dev/null @@ -1,80 +0,0 @@ -version: "0.7.0" -package: - name: contacts - version: "1.0.0" - description: A small contacts book API demonstrating interfaces, enums, and typed errors. - license: MIT - authors: - - WeaveFoundry - homepage: https://github.com/weavefoundry/weaveffi - repository: https://github.com/weavefoundry/weaveffi -modules: - - name: contacts - enums: - - name: ContactType - variants: - - name: Personal - value: 0 - - name: Work - value: 1 - - name: Other - value: 2 - structs: - - name: Contact - fields: - - name: id - type: i64 - - name: first_name - type: string - - name: last_name - type: string - - name: email - type: "string?" - - name: contact_type - type: ContactType - errors: - name: ContactsError - codes: - - { name: InvalidName, code: 1, message: "name must not be empty" } - - { name: NotFound, code: 2, message: "contact not found" } - interfaces: - - name: ContactBook - doc: An in-memory address book owning its contacts - constructors: - - name: new - params: [] - methods: - - name: add - doc: Add a contact, returning the stored record with its assigned id - params: - - name: first_name - type: string - - name: last_name - type: string - - name: email - type: "string?" - - name: contact_type - type: ContactType - return: Contact - throws: true - - name: get - doc: Look up a contact by id - params: - - name: id - type: i64 - return: Contact - throws: true - - name: list - doc: List every stored contact - params: [] - return: "[Contact]" - - name: remove - doc: Remove a contact by id, returning whether it existed - params: - - name: id - type: i64 - return: bool - - name: count - doc: Count the stored contacts - params: [] - return: i32 diff --git a/samples/contacts/weaveffi.toml b/samples/contacts/weaveffi.toml new file mode 100644 index 00000000..b8752e58 --- /dev/null +++ b/samples/contacts/weaveffi.toml @@ -0,0 +1,8 @@ +[package] +name = "contacts" +version = "1.0.0" +description = "A small contacts book API demonstrating interfaces, enums, and typed errors." +license = "MIT" +authors = ["WeaveFoundry "] +homepage = "https://github.com/weavefoundry/weaveffi" +repository = "https://github.com/weavefoundry/weaveffi" diff --git a/samples/events/events.yml b/samples/events/events.yml deleted file mode 100644 index b3e5a100..00000000 --- a/samples/events/events.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: "0.7.0" -package: - name: events - version: "1.0.0" - description: Callback and listener-based event subscription example. - license: MIT - authors: - - WeaveFoundry - homepage: https://github.com/weavefoundry/weaveffi - repository: https://github.com/weavefoundry/weaveffi -modules: - - name: events - callbacks: - - name: OnMessage - params: - - { name: message, type: string } - listeners: - - name: message_listener - event_callback: OnMessage - functions: - - name: send_message - doc: Send a message, triggering the OnMessage callback - params: - - { name: text, type: string } - - name: get_messages - doc: Return an iterator over all sent messages - params: [] - return: "iter" diff --git a/samples/events/weaveffi.toml b/samples/events/weaveffi.toml new file mode 100644 index 00000000..3069b858 --- /dev/null +++ b/samples/events/weaveffi.toml @@ -0,0 +1,8 @@ +[package] +name = "events" +version = "1.0.0" +description = "Callback and listener-based event subscription example." +license = "MIT" +authors = ["WeaveFoundry "] +homepage = "https://github.com/weavefoundry/weaveffi" +repository = "https://github.com/weavefoundry/weaveffi" diff --git a/samples/inventory/inventory.yml b/samples/inventory/inventory.yml deleted file mode 100644 index e9be8864..00000000 --- a/samples/inventory/inventory.yml +++ /dev/null @@ -1,139 +0,0 @@ -version: "0.7.0" -package: - name: inventory - version: "1.0.0" - description: Product and order inventory API spanning multiple modules. - license: MIT - authors: - - WeaveFoundry - homepage: https://github.com/weavefoundry/weaveffi - repository: https://github.com/weavefoundry/weaveffi -modules: - - name: products - enums: - - name: Category - variants: - - name: Electronics - value: 0 - - name: Clothing - value: 1 - - name: Food - value: 2 - - name: Books - value: 3 - structs: - - name: Product - fields: - - name: id - type: i64 - - name: name - type: string - - name: description - type: "string?" - - name: price - type: f64 - - name: category - type: Category - - name: tags - type: "[string]" - errors: - name: ProductsError - codes: - - { name: InvalidPrice, code: 1, message: "price must be positive" } - - { name: ProductNotFound, code: 2, message: "product not found" } - interfaces: - - name: Catalog - doc: A product catalog owning its products - constructors: - - name: new - params: [] - methods: - - name: add_product - doc: Add a product, returning the stored record with its assigned id - params: - - name: name - type: string - - name: price - type: f64 - - name: category - type: Category - return: Product - throws: true - - name: get_product - doc: Look up a product by id - params: - - name: id - type: i64 - return: Product - throws: true - - name: search - doc: Return every product in the given category - params: - - name: category - type: Category - return: "[Product]" - - name: update_price - doc: Update a product's price, returning whether the product existed - params: - - name: id - type: i64 - - name: price - type: f64 - return: bool - throws: true - - name: remove - doc: Remove a product by id, returning whether it existed - params: - - name: id - type: i64 - return: bool - - name: orders - structs: - - name: OrderItem - fields: - - name: product_id - type: i64 - - name: quantity - type: i32 - - name: unit_price - type: f64 - - name: Order - fields: - - name: id - type: i64 - - name: items - type: "[OrderItem]" - - name: total - type: f64 - - name: status - type: string - errors: - name: OrdersError - codes: - - { name: OrderNotFound, code: 1, message: "order not found" } - - { name: EmptyOrder, code: 2, message: "order must contain at least one item" } - functions: - - name: create_order - params: - - name: items - type: "[OrderItem]" - return: i64 - throws: true - - name: get_order - params: - - name: id - type: i64 - return: Order - throws: true - - name: cancel_order - params: - - name: id - type: i64 - return: bool - - name: add_product_to_order - params: - - name: order_id - type: i64 - - name: product - type: Product - return: bool diff --git a/samples/inventory/weaveffi.toml b/samples/inventory/weaveffi.toml new file mode 100644 index 00000000..ffc3b349 --- /dev/null +++ b/samples/inventory/weaveffi.toml @@ -0,0 +1,8 @@ +[package] +name = "inventory" +version = "1.0.0" +description = "Product and order inventory API spanning multiple modules." +license = "MIT" +authors = ["WeaveFoundry "] +homepage = "https://github.com/weavefoundry/weaveffi" +repository = "https://github.com/weavefoundry/weaveffi" diff --git a/samples/kvstore/kvstore.yml b/samples/kvstore/kvstore.yml deleted file mode 100644 index fc319ab8..00000000 --- a/samples/kvstore/kvstore.yml +++ /dev/null @@ -1,139 +0,0 @@ -version: "0.7.0" -package: - name: kvstore - version: "1.0.0" - description: An embedded key-value store API with TTLs, iteration, and async compaction. - license: MIT - authors: - - WeaveFoundry - homepage: https://github.com/weavefoundry/weaveffi - repository: https://github.com/weavefoundry/weaveffi -modules: - - name: kv - structs: - - name: Entry - doc: A single key-value entry persisted in the store - fields: - - { name: id, type: i64, doc: Stable monotonic identifier assigned on insert } - - { name: key, type: string, doc: UTF-8 lookup key } - - { name: value, type: bytes, doc: Opaque binary payload } - - { name: created_at, type: i64, doc: Unix-timestamp seconds when the entry was created } - - { name: expires_at, type: "i64?", doc: Optional unix-timestamp seconds at which the entry expires } - - { name: tags, type: "[string]", doc: Free-form labels attached to the entry } - - { name: metadata, type: "{string:string}", doc: Arbitrary string-valued metadata pairs } - enums: - - name: EntryKind - doc: Persistence semantics applied to a stored entry - variants: - - { name: Volatile, value: 0, doc: In-memory only; lost on close } - - { name: Persistent, value: 1, doc: Flushed to durable storage } - - { name: Encrypted, value: 2, doc: Persistent and encrypted at rest } - callbacks: - - name: OnEvict - doc: Fires when an entry is evicted from the store - params: - - { name: key, type: string } - listeners: - - name: eviction_listener - event_callback: OnEvict - doc: Subscribe to per-key eviction notifications - errors: - name: KvError - codes: - - { name: KeyNotFound, code: 1001, message: "key not found" } - - { name: Expired, code: 1002, message: "entry expired" } - - { name: StoreFull, code: 1003, message: "store has reached capacity" } - - { name: IoError, code: 1004, message: "I/O failure" } - interfaces: - - name: Store - doc: An embedded key-value store owning its entries - constructors: - - name: open - doc: Open (or create) a store backed by the given filesystem path - params: - - { name: path, type: string } - throws: true - methods: - - name: put - doc: Insert or replace a value, returning true on success - params: - - { name: key, type: string } - - { name: value, type: bytes } - - { name: kind, type: EntryKind } - - { name: ttl_seconds, type: "i64?" } - return: bool - throws: true - - name: get - doc: Look up an entry by key; returns null if missing or expired - params: - - { name: key, type: string } - return: "Entry?" - throws: true - - name: delete - doc: Remove the entry for the given key, returning true if it existed - params: - - { name: key, type: string } - return: bool - throws: true - - name: list_keys - doc: Stream every key, optionally filtered by a prefix - params: - - { name: prefix, type: "string?" } - return: "iter" - throws: true - - name: count - doc: Return the number of live entries in the store - params: [] - return: i64 - - name: clear - doc: Drop every entry from the store - params: [] - - name: compact - doc: Reclaim space asynchronously; returns the number of bytes reclaimed - params: [] - return: i64 - async: true - cancellable: true - throws: true - - name: legacy_put - doc: Legacy single-shot put kept for compatibility - deprecated: "use put() with explicit kind" - params: - - { name: key, type: string } - - { name: value, type: bytes } - return: bool - throws: true - statics: - - name: default_capacity - doc: The largest number of live entries one store will hold - params: [] - return: i64 - modules: - - name: stats - structs: - - name: Stats - doc: Aggregate store statistics - fields: - - { name: total_entries, type: i64, doc: Number of live entries } - - { name: total_bytes, type: i64, doc: Sum of all value byte lengths } - - { name: expired_entries, type: i64, doc: Number of entries past their TTL but not yet evicted } - functions: - - name: get_stats - doc: Snapshot the current store statistics - params: - - { name: store, type: Store } - return: Stats - throws: true -generators: - ruby: - module_name: Kvstore - dotnet: - namespace: Kvstore - swift: - module_name: Kvstore - cpp: - namespace: kvstore - dart: - package_name: kvstore - go: - module_path: github.com/example/kvstore diff --git a/samples/kvstore/weaveffi.toml b/samples/kvstore/weaveffi.toml new file mode 100644 index 00000000..b0bc541f --- /dev/null +++ b/samples/kvstore/weaveffi.toml @@ -0,0 +1,26 @@ +[package] +name = "kvstore" +version = "1.0.0" +description = "An embedded key-value store API with TTLs, iteration, and async compaction." +license = "MIT" +authors = ["WeaveFoundry "] +homepage = "https://github.com/weavefoundry/weaveffi" +repository = "https://github.com/weavefoundry/weaveffi" + +[generators.ruby] +module_name = "Kvstore" + +[generators.dotnet] +namespace = "Kvstore" + +[generators.swift] +module_name = "Kvstore" + +[generators.cpp] +namespace = "kvstore" + +[generators.dart] +package_name = "kvstore" + +[generators.go] +module_path = "github.com/example/kvstore" diff --git a/samples/shapes/shapes.yml b/samples/shapes/shapes.yml deleted file mode 100644 index d86df815..00000000 --- a/samples/shapes/shapes.yml +++ /dev/null @@ -1,67 +0,0 @@ -version: "0.7.0" -package: - name: shapes - version: "0.1.0" -modules: - - name: shapes - doc: Rich-enum + numerics smoke test - enums: - - name: Shape - doc: An algebraic shape (sum type with associated data) - variants: - - name: Empty - value: 0 - doc: The empty shape - - name: Circle - value: 1 - doc: A circle with a radius - fields: - - { name: radius, type: f64, doc: Radius in points } - - name: Rectangle - value: 2 - doc: An axis-aligned rectangle - fields: - - { name: width, type: f32 } - - { name: height, type: f32 } - - name: Labeled - value: 3 - doc: A labeled shape with a small count - fields: - - { name: label, type: string } - - { name: count, type: u8 } - - name: Channel - doc: A plain C-style enum (no payloads) - variants: - - { name: Red, value: 0 } - - { name: Green, value: 1 } - - { name: Blue, value: 2 } - functions: - - name: describe - doc: Render a shape to a string - params: - - { name: shape, type: Shape } - return: string - - name: scale - doc: Scale a shape by a factor, returning a new shape - params: - - { name: shape, type: Shape } - - { name: factor, type: f64 } - return: Shape - - name: sum_bytes - doc: Sum a list of bytes into a wide integer (numerics smoke) - params: - - { name: values, type: "[u8]" } - return: u64 -generators: - ruby: - module_name: Shapes - dotnet: - namespace: Shapes - swift: - module_name: Shapes - cpp: - namespace: shapes - dart: - package_name: shapes - go: - module_path: github.com/example/shapes diff --git a/samples/shapes/src/lib.rs b/samples/shapes/src/lib.rs index c3fe506d..f3512013 100644 --- a/samples/shapes/src/lib.rs +++ b/samples/shapes/src/lib.rs @@ -8,7 +8,7 @@ //! for the type instead of emitting per-variant C symbols. `Channel` is a //! plain C-style enum that crosses as its `i32` discriminant. The producer //! writes only safe Rust; the macro emits the `weaveffi_shapes_*` thunks that -//! line up 1:1 with the generated header (see `weaveffi generate shapes.yml +//! line up 1:1 with the generated header (see `weaveffi generate src/lib.rs //! --target c`). /// Rich-enum + numerics smoke test diff --git a/samples/shapes/weaveffi.toml b/samples/shapes/weaveffi.toml new file mode 100644 index 00000000..27e6e9c0 --- /dev/null +++ b/samples/shapes/weaveffi.toml @@ -0,0 +1,21 @@ +[package] +name = "shapes" +version = "0.1.0" + +[generators.ruby] +module_name = "Shapes" + +[generators.dotnet] +namespace = "Shapes" + +[generators.swift] +module_name = "Shapes" + +[generators.cpp] +namespace = "shapes" + +[generators.dart] +package_name = "shapes" + +[generators.go] +module_path = "github.com/example/shapes" diff --git a/weaveffi.schema.json b/weaveffi.schema.json index a725ca8d..e2f99d5f 100644 --- a/weaveffi.schema.json +++ b/weaveffi.schema.json @@ -9,100 +9,36 @@ ], "properties": { "version": { - "description": "IR schema version this document targets (for example `0.7.0`). Validation rejects any value not listed in [`SUPPORTED_VERSIONS`].", + "description": "IR schema version this document targets (for example `0.8.0`). Validation rejects any value not listed in [`SUPPORTED_VERSIONS`].", "type": "string" }, - "package": { - "description": "Package identity used to name, version, and describe every generated consumer package (npm, PyPI, gem, NuGet, pub.dev, SwiftPM, Gradle, Go). When omitted, generators fall back to the IDL file stem and version `0.1.0`, but publishable artifacts should always set this explicitly.", - "anyOf": [ - { - "$ref": "#/definitions/Package" - }, - { - "type": "null" - } - ] - }, "modules": { "description": "Top-level modules that make up the API surface. Each is an independent namespace; modules may nest further through [`Module::modules`].", "type": "array", "items": { "$ref": "#/definitions/Module" } - }, - "generators": { - "description": "Per-generator configuration keyed by backend name (for example `swift` or `python`). The opaque [`toml::Value`] payload is interpreted by each generator, so unrecognized keys pass through untouched. `None` when the IDL declares no `generators:` block.", - "type": [ - "object", - "null" - ], - "additionalProperties": true } }, + "additionalProperties": false, "definitions": { - "Package": { - "description": "Package identity for the generated consumer artifacts.", + "Module": { + "description": "A WeaveFFI module: a named group of functions, types, callbacks, listeners, and errors.", "type": "object", "required": [ - "name", - "version" + "name" ], "properties": { "name": { - "description": "Canonical package name (e.g. `kvstore`). Per-target name overrides in `generators:` (such as `python.package_name`) still take precedence.", - "type": "string" - }, - "version": { - "description": "Semantic version stamped into every manifest (e.g. `1.2.0`).", + "description": "Module name, used as a namespace segment and a symbol-prefix component in generated code (for example `contacts`).", "type": "string" }, - "description": { - "description": "Short summary written into each manifest's description field. Omitted from generated manifests when absent.", - "type": [ - "string", - "null" - ] - }, - "license": { - "description": "License identifier, typically an SPDX expression such as `MIT` or `Apache-2.0`, written into each manifest's license field.", - "type": [ - "string", - "null" - ] - }, - "authors": { - "description": "Package authors, each commonly formatted as `Name `, mapped to whatever author or maintainer field the target ecosystem uses. Empty by default.", - "type": "array", - "items": { - "type": "string" - } - }, - "homepage": { - "description": "Project homepage URL recorded in manifests that expose one.", - "type": [ - "string", - "null" - ] - }, - "repository": { - "description": "Source repository URL recorded in manifests that expose one.", + "doc": { + "description": "Human-readable documentation for the module as a whole, propagated to the generated namespace, class, or file header. `None` when undocumented.", "type": [ "string", "null" ] - } - } - }, - "Module": { - "description": "A WeaveFFI module: a named group of functions, types, callbacks, listeners, and errors.", - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "description": "Module name, used as a namespace segment and a symbol-prefix component in generated code (for example `contacts`).", - "type": "string" }, "functions": { "description": "Free functions this module exports across the FFI boundary.", @@ -286,6 +222,13 @@ "null" ] }, + "deprecated": { + "description": "Deprecation notice for the whole type; when set, generators annotate the emitted class. `None` means the interface is current.", + "type": [ + "string", + "null" + ] + }, "constructors": { "description": "Constructors: static functions returning a new instance. A constructor declares no `return` (the instance is implicit) and may not be `async`.", "type": "array", @@ -328,6 +271,13 @@ "null" ] }, + "deprecated": { + "description": "Deprecation notice for the whole type; when set, generators annotate the emitted record. `None` means the struct is current.", + "type": [ + "string", + "null" + ] + }, "fields": { "description": "The fields in declaration order; order is preserved in the generated type, its constructors, and its serialized buffer encoding.", "type": "array", @@ -385,6 +335,13 @@ "null" ] }, + "deprecated": { + "description": "Deprecation notice for the whole type; when set, generators annotate the emitted enum. `None` means the enum is current.", + "type": [ + "string", + "null" + ] + }, "variants": { "description": "The variants in declaration order. Whether any of them carries fields decides if this is a C-style or an algebraic enum.", "type": "array",